diff --git a/docs/.vuepress/config.js b/docs/.vuepress/config.js index b92a1c3..2155145 100644 --- a/docs/.vuepress/config.js +++ b/docs/.vuepress/config.js @@ -3,6 +3,9 @@ module.exports = { description: 'Offical blog plugin for VuePress', themeConfig: { repo: 'ulivz/vuepress-plugin-blog', + nav: [ + { text: 'Config', link: '/config/' } + ] } } diff --git a/docs/README.md b/docs/README.md index 100862a..25652eb 100644 --- a/docs/README.md +++ b/docs/README.md @@ -4,7 +4,7 @@ sidebar: auto # @vuepress/plugin-blog -> Official blog plugin for VuePress. +> Official blog plugin for VuePress. Note that this plugin has been deeply integrated into [@vuepress/theme-blog](https://github.com/ulivz/vuepress-theme-blog). @@ -19,7 +19,12 @@ yarn add -D @vuepress/plugin-blog ```javascript module.exports = { - plugins: ['@vuepress/blog', { /* options */ }] + plugins: [ + '@vuepress/blog', + { + /* options */ + }, + ], // Please keep looking down to see the available options. } ``` @@ -31,25 +36,22 @@ module.exports = { `Document classifier` is a set of functions that can classify pages with the same characteristics. For a blog developer, the same characteristics may exist between different pages as follows: - Pages put in a directory (e.g. `_post`) -- Pages containing the same specific frontmatter key value (e.g. `tag: js`). +- Pages containing the same specific frontmatter key value (e.g. `tag: js`). -Of course, both of them may be related to another common +Of course, both of them may be related to another common requirement, `pagination`. So, how to combine them skillfully? Next, let's take a look at how this plugin solve these problems. - ### Directory Classifier Directory Classifier, that classifies the source pages placed in a same directory. Suppose you have the following files structure: -``` vue -. -└── _posts -    ├── 2018-4-4-intro-to-vuepress.md -    └── 2019-6-8-intro-to-vuepress-next.md +```vue +. └── _posts    ├── 2018-4-4-intro-to-vuepress.md    └── +2019-6-8-intro-to-vuepress-next.md ``` In the traditional VuePress site, the converted page URLs will be: @@ -63,33 +65,36 @@ It doesn't seem blogging, so it's time to use this plugin: // .vuepress/config.js module.exports = { plugins: [ - ['@vuepress/blog', { - directories: [ - { - // Unique ID of current classification - id: 'post', - // Target directory - dirname: '_posts', - // Path of the `entry page` (or `list page`) - path: '/', - }, - ], - }] - ] + [ + '@vuepress/blog', + { + directories: [ + { + // Unique ID of current classification + id: 'post', + // Target directory + dirname: '_posts', + // Path of the `entry page` (or `list page`) + path: '/', + }, + ], + }, + ], + ], } ``` -Then the plugin will help you to generate following pages, and automatically leverage corresponding +Then the plugin will help you to generate following pages, and automatically leverage corresponding layout: -| url | layout | -|---|---| -| `/` | `IndexPost` / `Layout` | -| `/2018/04/04/intro-to-vuepress/` | `Post` | -| `/2019/06/08/intro-to-vuepress-next/` | `Post` | +| url | layout | +| ------------------------------------- | ---------------------- | +| `/` | `IndexPost` / `Layout` | +| `/2018/04/04/intro-to-vuepress/` | `Post` | +| `/2019/06/08/intro-to-vuepress-next/` | `Post` | This means that you need to create two layout components(`IndexPost` and `Post`) to handle the layout of index and post -pages. +pages. You can also custom the layout component name: @@ -134,7 +139,7 @@ module.exports = { ``` ::: warning -It is noteworthy that the `path` and `itemPermalink` must be uniformly modified, and `itemPermalink` must be prefixed with +It is noteworthy that the `path` and `itemPermalink` must be uniformly modified, and `itemPermalink` must be prefixed with `path`. The default value of `itemPermalink` is `'/:year/:month/:day/:slug'`. @@ -143,8 +148,8 @@ The default value of `itemPermalink` is `'/:year/:month/:day/:slug'`. ### Pagination As your blog articles grew more and more, you began to have the need for paging. By default, this plugin integrates a - very powerful pagination system that allows you to access paging functions with simple configuration. - +very powerful pagination system that allows you to access paging functions with simple configuration. + By default, the plugin assumes that the max number of pages per page is `10`. you can also modify it like this: ```diff @@ -175,23 +180,23 @@ Suppose you have 3 pages at `_posts` direcotry: When the `perPagePosts` is set to `2`, this plugin will help you generate the following pages: -| url | layout | -|---|---| -| `/` | `IndexPost` / `Layout` | +| url | layout | +| ---------------- | -------------------------------- | +| `/` | `IndexPost` / `Layout` | | `/page/2/` (New) | `DirectoryPagination` / `Layout` | -| `/2019/06/08/a/` | `Post` | -| `/2019/06/08/b/` | `Post` | -| `/2018/06/08/c/` | `Post` | +| `/2019/06/08/a/` | `Post` | +| `/2019/06/08/b/` | `Post` | +| `/2018/06/08/c/` | `Post` | -::: tip +::: tip `DirectoryPagination / Layout` means that the layout component will be downgraded to `Layout` when `DirectoryPagination` layout doesn't exist. -::: +::: So how to get the matched pages in the layout component? In fact, it will be much simpler than you think. If you visit `/`, current page leverages layout `IndexPost`. In this layout component, you just need to use `this.$pagination.pages` to get the matched pages. In the above example, the actual value of `this.$pagination.pages` will - be: +be: ```json [ @@ -211,13 +216,11 @@ If you visit `/`, current page leverages layout `DirectoryPagination`, you can a Isn't this very natural experience? You just need to care about the style of your layout component, not the complicated and boring logic behind it. - ::: tip To save the length of docs, we omitted the data structure of the `$page` object. You can get more information about the data structure of `$page` at the [official documentation](https://v1.vuepress.vuejs.org/guide/global-computed.html#page). ::: - ### Frontmatter Classifier Frontmatter Classifier, which classifies pages that have the same frontmatter key values. @@ -253,32 +256,35 @@ If you want to easily classify them, you can config like this: ```js module.exports = { plugins: [ - ['@vuepress/blog', { - frontmatters: [ - { - // Unique ID of current classification - id: "tag", - // Decide that the frontmatter keys will be grouped under this classification - keys: ['tag'], - // Path of the `entry page` (or `list page`) - path: '/tag/', - // Layout of the `entry page` - layout: 'Tag', - }, - ] - }] - ] + [ + '@vuepress/blog', + { + frontmatters: [ + { + // Unique ID of current classification + id: 'tag', + // Decide that the frontmatter keys will be grouped under this classification + keys: ['tag'], + // Path of the `entry page` (or `list page`) + path: '/tag/', + // Layout of the `entry page` + layout: 'Tag', + }, + ], + }, + ], + ], } ``` Then the plugin will help you to generate the following extra pages, and automatically leverage the corresponding layout: -| url | layout | -|---|---| -| `/tag/` | `Tag` | +| url | layout | +| ----------- | ---------------------------------- | +| `/tag/` | `Tag` | | `/tag/vue/` | `FrontmatterPagination` / `Layout` | -| `/tag/js/` | `FrontmatterPagination` / `Layout` | +| `/tag/js/` | `FrontmatterPagination` / `Layout` | In the `Tags` component, you can use `this.$tag.list` to get the tag list. The value would be like: @@ -302,9 +308,9 @@ In the `Tags` component, you can use `this.$tag.list` to get the tag list. The v ] ``` -In the `FrontmatterPagination` component, you can use `this.$pagination.pages` to get the matched pages in current tag +In the `FrontmatterPagination` component, you can use `this.$pagination.pages` to get the matched pages in current tag classification: - + - If you visit `/tag/vue/`, the `this.$pagination.pages` will be: ```json @@ -322,7 +328,6 @@ classification: ] ``` - ## Examples Actually, there are only 2 necessary layout components to create a blog theme: @@ -332,17 +337,3 @@ Actually, there are only 2 necessary layout components to create a blog theme: - Tag (Only required when you set up a `tag` frontmatter classification.) Here is [live example](https://github.com/ulivz/70-lines-of-vuepress-blog-theme) that implements a functionally qualified VuePress theme in around 70 lines. - -## Options - -### directories - -> TODO, contribution welcome. - -### frontmatters - -> TODO, contribution welcome. - - - - diff --git a/docs/config/README.md b/docs/config/README.md new file mode 100644 index 0000000..f6d041a --- /dev/null +++ b/docs/config/README.md @@ -0,0 +1,106 @@ +--- +sidebar: auto +--- + +# Config + +## directories + +- Type: `DirectoryClassifier[]` +- Default: `[]` + +Create one or more [directory classifiers](../README.md#directory-classifier), all available options in +`DirectoryClassifier` are as +follows. + +### id + +- Type: `string` +- Default: `undefined` +- Required: `true` + +Unique id for current classifier, e.g. `post`. + +### dirname + +- Type: `string` +- Default: `undefined` +- Required: `true` + +Matched directory name, e.g. `_post`. + +### path + +- Type: `string` +- Default: `/${id}/` +- Required: `false` + +Entry page for current classifier, e.g. `/` or `/post/`. + +If you set `DirectoryClassifier.path` to `/`, it means that you want to access the matched pages list at `/`. set +to `/post/` is the same. + +### layout + +- Type: `string` +- Default: `'IndexPost' || 'Layout'` +- Required: `false` + +Layout component name for entry page. + +### frontmatter + +- Type: `Record` +- Default: `{}` +- Required: `false` + +[Frontmatter](https://v1.vuepress.vuejs.org/guide/frontmatter.html) for entry page. + +### itemLayout + +- Type: `string` +- Default: `'Post'` +- Required: `false` + +Layout for matched pages. + +### itemPermalink + +- Type: `string` +- Default: `'/:year/:month/:day/:slug'` +- Required: `false` + +Permalink for matched pages. + +For example, if you set up a directory classifier with dirname equals to `_post`, and have following pages: + +``` +. +└── _posts + ├── 2018-4-4-intro-to-vuepress.md + └── 2019-6-8-intro-to-vuepress-next.md +``` + +With the default `itemPermalink`, you'll get following output paths: + +``` +/2018/04/04/intro-to-vuepress/ +/2019/06/08/intro-to-vuepress-next/ +``` + +For more details about permalinks, please head to [Permalinks](https://v1.vuepress.vuejs.org/guide/permalinks.html) section at VuePress's documentation. + +### pagination + +- Type: `Pagination` +- Default: `{ perPagePosts: 10 }` +- Required: `false` + +All available options of pagination are as follows: + +- pagination.perPagePosts: Maximum number of posts per page. +- pagination.pagesSorter: Maximum number of posts per page. + +## frontmatters + +> TODO, contribution welcome. diff --git a/package.json b/package.json index 74720a5..9187ddd 100644 --- a/package.json +++ b/package.json @@ -4,7 +4,7 @@ "description": "Offical blog plugin for VuePress", "scripts": { "lint": "xo", - "dev": "tsc -skipLibCheck --watch", + "dev": "npm run cpc && tsc -skipLibCheck --watch", "cpc": "cp -r src/client lib", "build": "tsc -skipLibCheck && npm run cpc", "dev:docs": "vuepress dev docs --temp docs/.temp", @@ -26,21 +26,22 @@ "author": "ULIVZ ", "license": "MIT", "devDependencies": { - "xo": "^0.23.0", - "prettier": "^1.15.2", - "eslint-config-rem": "^4.0.0", + "conventional-changelog-cli": "^2.0.1", "eslint-config-prettier": "^3.3.0", - "eslint-plugin-prettier": "^3.0.0", + "eslint-config-rem": "^4.0.0", "eslint-config-xo-typescript": "^0.3.0", + "eslint-plugin-prettier": "^3.0.0", "eslint-plugin-typescript": "^0.14.0", - "typescript-eslint-parser": "^21.0.2", "husky": "^1.2.0", "lint-staged": "^8.1.0", "nodemon": "^1.18.7", + "prettier": "^1.15.2", "ts-node": "^7.0.1", "typescript": "^3.1.4", - "conventional-changelog-cli": "^2.0.1", - "vuepress": "next" + "typescript-eslint-parser": "^21.0.2", + "vuepress": "^1.0.0", + "xo": "^0.23.0", + "vuejs-paginate": "^2.1.0" }, "xo": { "extends": [ diff --git a/src/client/classification.js b/src/client/classification.js index 6668f6c..6897af7 100644 --- a/src/client/classification.js +++ b/src/client/classification.js @@ -52,7 +52,7 @@ export default ({ Vue }) => { return classified }, [`$current${classifiedType.charAt(0).toUpperCase() + - classifiedType.slice(1)}`]() { + classifiedType.slice(1)}`]() { const tagName = this.$route.meta.pid return this[helperName].getItemByName(tagName) }, diff --git a/src/client/init.js b/src/client/init.js new file mode 100644 index 0000000..4e14069 --- /dev/null +++ b/src/client/init.js @@ -0,0 +1,2 @@ +export Pagination from '../components/Pagination' +export SimplePagination from '../components/SimplePagination' diff --git a/src/client/pagination.js b/src/client/pagination.js index 401e643..a4949bc 100644 --- a/src/client/pagination.js +++ b/src/client/pagination.js @@ -1,20 +1,9 @@ import Vue from 'vue' import paginations from '@dynamic/vuepress_blog/paginations' -import frontmatterClassifications from '@dynamic/vuepress_blog/frontmatterClassifications' -import pageFilters from '@dynamic/vuepress_blog/pageFilters' -import pageSorters from '@dynamic/vuepress_blog/pageSorters' import _debug from 'debug' const debug = _debug('plugin-blog:pagination') -function getClientFrontmatterPageFilter(rawFilter, pid, value) { - // debug('getClientFrontmatterPageFilter') - // debug('frontmatterClassifications', frontmatterClassifications) - // debug('pid', pid) - const match = frontmatterClassifications.filter(i => i.id === pid)[0] - return page => rawFilter(page, match && match.keys, value) -} - class PaginationGateway { constructor(paginations) { this.paginations = paginations @@ -39,11 +28,7 @@ const gateway = new PaginationGateway(paginations) class Pagination { constructor(pagination, pages, route) { debug(pagination) - const { pid, id, paginationPages } = pagination - - const pageFilter = getClientFrontmatterPageFilter(pageFilters[pid], pid, id) - const pageSorter = pageSorters[pid] - + const { pages: paginationPages } = pagination const { path } = route for (let i = 0, l = paginationPages.length; i < l; i++) { @@ -60,7 +45,7 @@ class Pagination { this._paginationPages = paginationPages this._currentPage = paginationPages[this.paginationIndex] - this._matchedPages = pages.filter(pageFilter).sort(pageSorter) + this._matchedPages = pages.filter(pagination.filter).sort(pagination.sorter) } setIndexPage(path) { @@ -98,6 +83,10 @@ class Pagination { return this._paginationPages[this.paginationIndex + 1].path } } + + getSpecificPageLink(index) { + return this._paginationPages[this.paginationIndex + 1].path + } } export default ({ Vue }) => { @@ -114,11 +103,8 @@ export default ({ Vue }) => { return {} } - return this.$getPagination( - this.$route.meta.pid, - this.$route.meta.id, - ) + return this.$getPagination(this.$route.meta.pid, this.$route.meta.id) }, - } + }, }) } diff --git a/src/components/Pagination.vue b/src/components/Pagination.vue new file mode 100644 index 0000000..3893d23 --- /dev/null +++ b/src/components/Pagination.vue @@ -0,0 +1,135 @@ + + + + + diff --git a/src/components/SimplePagination.vue b/src/components/SimplePagination.vue new file mode 100644 index 0000000..6a471f4 --- /dev/null +++ b/src/components/SimplePagination.vue @@ -0,0 +1,4 @@ + diff --git a/src/handleOptions.ts b/src/handleOptions.ts index ecbb0a1..81bd8a2 100644 --- a/src/handleOptions.ts +++ b/src/handleOptions.ts @@ -2,10 +2,15 @@ import { BlogPluginOptions } from './interface/Options' import { ExtraPage } from './interface/ExtraPages' import { PageEnhancer } from './interface/PageEnhancer' import { AppContext } from './interface/VuePress' -import { InternalPagination } from './interface/Pagination' +import { InternalPagination, PaginationConfig } from './interface/Pagination' import { FrontmatterClassificationPage } from './interface/Frontmatter' -import { curryFrontmatterHandler, FrontmatterTempMap } from './util' -import { DefaultLayoutEnum } from './Config' +import { + curryFrontmatterHandler, + FrontmatterTempMap, + resolvePaginationConfig, + UpperFirstChar, +} from './util' +import { ClassifierTypeEnum } from './interface/Classifier' /** * Handle options from users. @@ -15,22 +20,8 @@ import { DefaultLayoutEnum } from './Config' */ export function handleOptions(options: BlogPluginOptions, ctx: AppContext) { - const { layoutComponentMap } = ctx.themeAPI - const { directories = [], frontmatters = [] } = options - /** - * A function used to check whether layout exists - */ - const isLayoutExists = name => layoutComponentMap[name] !== undefined - - /** - * Get layout - */ - const getLayout = (name?: string, fallback?: string) => { - return isLayoutExists(name) ? name : fallback || 'Layout' - } - const pageEnhancers: PageEnhancer[] = [] const frontmatterClassificationPages: FrontmatterClassificationPage[] = [] const extraPages: ExtraPage[] = [] @@ -43,14 +34,14 @@ export function handleOptions(options: BlogPluginOptions, ctx: AppContext) { const { id, dirname, - path: indexPath, + path: indexPath = `/${directory.id}/`, layout: indexLayout = 'IndexPost', frontmatter, itemLayout = 'Post', itemPermalink = '/:year/:month/:day/:slug', pagination = { - perPagePosts: 10, - }, + lengthPerPage: 10, + } as PaginationConfig, } = directory /** @@ -65,23 +56,20 @@ export function handleOptions(options: BlogPluginOptions, ctx: AppContext) { */ extraPages.push({ permalink: indexPath, - frontmatter, + frontmatter: { + // Set layout for index page. + layout: ctx.getLayout(indexLayout), + title: `${UpperFirstChar(id)}`, + ...frontmatter, + }, meta: { pid: id, - id: id, + id, }, }) /** - * 1.3 Set layout for index page. - */ - pageEnhancers.push({ - when: ({ regularPath }) => regularPath === indexPath, - frontmatter: { layout: getLayout(indexLayout) }, - }) - - /** - * 1.4 Set layout for pages that match current directory pattern. + * 1.3 Set layout for pages that match current directory classifier. */ pageEnhancers.push({ when: ({ regularPath }) => @@ -89,7 +77,7 @@ export function handleOptions(options: BlogPluginOptions, ctx: AppContext) { regularPath !== indexPath && regularPath.startsWith(`/${dirname}/`), frontmatter: { - layout: getLayout(itemLayout, 'Post'), + layout: ctx.getLayout(itemLayout, 'Post'), permalink: itemPermalink, }, data: { id, pid: id }, @@ -99,22 +87,20 @@ export function handleOptions(options: BlogPluginOptions, ctx: AppContext) { * 1.5 Set pagination. */ paginations.push({ + classifierType: ClassifierTypeEnum.Directory, + getPaginationPageTitle(index) { + return `Page ${index + 1} | ${id}` + }, + ...resolvePaginationConfig( + ClassifierTypeEnum.Directory, + pagination, + indexPath, + id, + id, + ctx, + ), pid: id, id, - meta: { - pid: id, - id: id, - }, - options: { - ...pagination, - layout: DefaultLayoutEnum.DirectoryPagination, - }, - getUrl(index) { - if (index === 0) { - return indexPath - } - return `${indexPath}page/${index + 1}/` - }, }) } @@ -129,8 +115,8 @@ export function handleOptions(options: BlogPluginOptions, ctx: AppContext) { layout: indexLayout, frontmatter, pagination = { - perPagePosts: 10, - }, + lengthPerPage: 10, + } as PaginationConfig, } = frontmatterPage if (!indexPath) { @@ -139,7 +125,12 @@ export function handleOptions(options: BlogPluginOptions, ctx: AppContext) { extraPages.push({ permalink: indexPath, - frontmatter, + frontmatter: { + // Set layout for index page. + layout: ctx.getLayout(indexLayout), + title: `${UpperFirstChar(id)}`, + ...frontmatter, + }, }) const map = {} as FrontmatterTempMap @@ -151,11 +142,6 @@ export function handleOptions(options: BlogPluginOptions, ctx: AppContext) { map, _handler: curryFrontmatterHandler(id, map), }) - - pageEnhancers.push({ - when: ({ regularPath }) => regularPath === indexPath, - frontmatter: { layout: getLayout(indexLayout) }, - }) } return { diff --git a/src/index.ts b/src/index.ts index 273dfea..45e2c5b 100644 --- a/src/index.ts +++ b/src/index.ts @@ -3,10 +3,28 @@ import { handleOptions } from './handleOptions' import { registerPagination } from './pagination' import { BlogPluginOptions } from './interface/Options' import { AppContext, Page } from './interface/VuePress' -import { DefaultLayoutEnum } from './Config' -import { logPages } from './util' +import { logPages, resolvePaginationConfig } from './util' +import { ClassifierTypeEnum, DefaultLayoutEnum } from './interface/Classifier' + +function injectExtraAPI(ctx: AppContext) { + const { layoutComponentMap } = ctx.themeAPI + + /** + * A function used to check whether layout exists + */ + const isLayoutExists = name => layoutComponentMap[name] !== undefined + + /** + * Get layout + */ + ctx.getLayout = (name?: string, fallback?: string) => { + return isLayoutExists(name) ? name : fallback || 'Layout' + } +} module.exports = (options: BlogPluginOptions, ctx: AppContext) => { + injectExtraAPI(ctx) + const { pageEnhancers, frontmatterClassificationPages, @@ -84,34 +102,24 @@ module.exports = (options: BlogPluginOptions, ctx: AppContext) => { /** * Register pagination */ + const indexPath = `/${scope}/${key}/` + paginations.push({ - pid: scope, - id: key, - meta: { - pid: scope, - id: key, - }, - options: { - ...pagination, - layout: DefaultLayoutEnum.FrontmatterPagination, - serverPageFilter(page) { - return clientFrontmatterClassifierPageFilter( - page, - keys, - key, - ) - }, - clientPageFilter: clientFrontmatterClassifierPageFilter, - }, - getUrl(index) { - if (index === 0) { - return `/${scope}/${key}/` - } - return `/${scope}/${key}/page/${index + 1}/` - }, - getTitle(index) { + classifierType: ClassifierTypeEnum.Frontmatter, + getPaginationPageTitle(index) { return `Page ${index + 1} - ${key} | ${scope}` }, + ...resolvePaginationConfig( + ClassifierTypeEnum.Frontmatter, + pagination, + indexPath, + scope, + key, + ctx, + keys, + ), + pid: scope, + id: key, }) return { @@ -124,7 +132,7 @@ module.exports = (options: BlogPluginOptions, ctx: AppContext) => { id: key, frontmatter: { layout: DefaultLayoutEnum.FrontmatterPagination, - title: `${key} | ${scope}`, + title: `${key} ${scope}`, }, } }) @@ -132,10 +140,7 @@ module.exports = (options: BlogPluginOptions, ctx: AppContext) => { .reduce((arr, next) => arr.concat(next), []), ] - logPages( - `Automatically Added Index Pages`, - allExtraPages - ) + logPages(`Automatically Added Index Pages`, allExtraPages) await Promise.all(allExtraPages.map(async page => ctx.addPage(page))) await registerPagination(paginations, ctx) @@ -154,25 +159,8 @@ module.exports = (options: BlogPluginOptions, ctx: AppContext) => { ) const PREFIX = 'vuepress_blog' - const strippedFrontmatterClassificationPages = frontmatterClassificationPages.map( - ({ id, pagination, keys }) => { - return { - id, - pagination, - keys, - } - }, - ) return [ - { - name: `${PREFIX}/frontmatterClassifications.js`, - content: `export default ${JSON.stringify( - strippedFrontmatterClassificationPages, - null, - 2, - )}`, - }, { name: `${PREFIX}/frontmatterClassified.js`, content: `export default ${JSON.stringify( @@ -183,15 +171,23 @@ module.exports = (options: BlogPluginOptions, ctx: AppContext) => { }, { name: `${PREFIX}/paginations.js`, - content: `export default ${JSON.stringify(ctx.paginations, null, 2)}`, + content: ` +import sorters from './pageSorters' +import filters from './pageFilters' + +export default ${serializePaginations(ctx.serializedPaginations, [ + 'filter', + 'sorter', + ])} +`, }, { name: `${PREFIX}/pageFilters.js`, - content: `export default ${mapToString(ctx.pageFilters)}`, + content: `export default ${mapToString(ctx.pageFilters, true)}`, }, { name: `${PREFIX}/pageSorters.js`, - content: `export default ${mapToString(ctx.pageSorters)}`, + content: `export default ${mapToString(ctx.pageSorters, true)}`, }, ] }, @@ -203,21 +199,26 @@ module.exports = (options: BlogPluginOptions, ctx: AppContext) => { } } -function mapToString(map) { +function serializePaginations(paginations, unstringedKeys: string[] = []) { + return `[${paginations.map(p => mapToString(p, unstringedKeys)).join(',\n')}]` +} + +/** + * Transform map tp string. + * + * @param map + * @param unstringedKeys Set to ture to force all field value to not be stringified. + */ +function mapToString(map, unstringedKeys: string[] | boolean = []) { + const keys = unstringedKeys let str = `{\n` for (const key of Object.keys(map)) { - str += ` "${key}": ${map[key]},\n` + str += ` ${key}: ${ + keys === true || (Array.isArray(keys) && keys.includes(key)) + ? map[key] + : JSON.stringify(map[key]) + },\n` } str += '}' return str } - -function clientFrontmatterClassifierPageFilter(page, keys, value) { - return keys.some(key => { - const _value = page.frontmatter[key] - if (Array.isArray(_value)) { - return _value.some(i => i === value) - } - return _value === value - }) -} diff --git a/src/Config.ts b/src/interface/Classifier.ts similarity index 58% rename from src/Config.ts rename to src/interface/Classifier.ts index 4ce1cfc..e3d2ff2 100644 --- a/src/Config.ts +++ b/src/interface/Classifier.ts @@ -1,3 +1,8 @@ +export enum ClassifierTypeEnum { + Directory = 'Directory', + Frontmatter = 'Frontmatter', +} + export enum DefaultLayoutEnum { FrontmatterPagination = 'FrontmatterPagination', DirectoryPagination = 'DirectoryPagination', diff --git a/src/interface/Frontmatter.ts b/src/interface/Frontmatter.ts index 29085ac..d160ffa 100644 --- a/src/interface/Frontmatter.ts +++ b/src/interface/Frontmatter.ts @@ -1,5 +1,5 @@ import { FrontmatterHandler } from '../util' -import { PaginationConfig } from './Options' +import { PaginationConfig } from './Pagination' export interface FrontmatterClassificationPage { id: string; diff --git a/src/interface/Options.ts b/src/interface/Options.ts index cc42067..a054df7 100644 --- a/src/interface/Options.ts +++ b/src/interface/Options.ts @@ -1,19 +1,8 @@ -/** - * Config of a Pagination - */ -export interface PaginationConfig { - postsFilter?: typeof Array.prototype.filter - postsSorter?: typeof Array.prototype.sort - perPagePosts?: number - layout?: string - serverPageFilter?: any; - clientPageFilter?: any; - clientPageSorter?: any; -} - /** * A Directory-based Classifier */ +import { PaginationConfig } from './Pagination' + export interface DirectoryClassifier { /** * Unique id for current classifier. @@ -24,15 +13,15 @@ export interface DirectoryClassifier { */ dirname: string; /** - * Index page for current classifier. + * Entry page for current classifier. */ path: string; /** - * Layout for index page. + * Layout component name for entry page. */ layout?: string; /** - * Frontmatter for index page. + * Frontmatter for entry page. */ frontmatter?: Record; /** diff --git a/src/interface/Pagination.ts b/src/interface/Pagination.ts index b74738e..1849ce3 100644 --- a/src/interface/Pagination.ts +++ b/src/interface/Pagination.ts @@ -1,11 +1,95 @@ -import { PaginationConfig } from './Options' +/** + * Config of a Pagination + */ +import { Page } from './VuePress' +import { ClassifierTypeEnum } from './Classifier' -export interface InternalPagination { +export type PageFilter = (page: Page) => boolean +export type PageSorter = (prev: Page, next: Page) => boolean | number +export type GetPaginationPageUrl = (index: number) => string +export type getPaginationPageTitle = (index: number) => string + +/** + * Pagination config options for users. + */ +export interface PaginationConfig { + /** + * Filter for matched pages. + */ + filter?: PageFilter; + /** + * Sorter for matched pages. + */ + sorter?: PageSorter; + /** + * Maximum number of posts per page. + */ + lengthPerPage?: number; + /** + * Layout for pagination Page (Except the index page.) + */ + layout?: string; + /** + * A function to get the url of pagination page dynamically. + */ + getPaginationPageUrl?: GetPaginationPageUrl; + /** + * A function to get the title of pagination page dynamically. + */ + getPaginationPageTitle?: getPaginationPageTitle; +} + +export interface PaginationIdentity { + /** + * Generalized ID + */ pid: string; + /** + * Narrow ID + */ id: string; - layout?: string; - meta?: Record; - options: PaginationConfig; - getUrl: (index: number) => string; - getTitle?: any; +} + +/** + * Internally used fields. + */ +export interface InternalPagination + extends PaginationConfig, + PaginationIdentity { + /** + * Record which classfier create this pagination. + */ + classifierType: ClassifierTypeEnum; +} + +/** + * Serialized pagination, generated for front-end use + */ +export interface SerializedPagination extends PaginationIdentity { + /** + * Stringified filter function + */ + filter: string; + /** + * Stringified sorter function + */ + sorter: string; + /** + * Details under current pagination + */ + pages: PaginationPage[]; +} + +/** + * Auto-generated pagination page + */ +interface PaginationPage { + /** + * Path of current pagination page + */ + path: string; + /** + * Store the first and last page index matched + */ + interval: Array; } diff --git a/src/interface/VuePress.ts b/src/interface/VuePress.ts index abc18f7..eac4ffb 100644 --- a/src/interface/VuePress.ts +++ b/src/interface/VuePress.ts @@ -1,6 +1,6 @@ import Vue from 'vue' import { FrontmatterClassificationPage } from './Frontmatter' -import { InternalPagination } from './Pagination' +import { SerializedPagination } from './Pagination' export interface Page { key: string; @@ -18,7 +18,8 @@ export interface AppContext { export interface AppContext { frontmatterClassificationPages: FrontmatterClassificationPage[]; - paginations: InternalPagination[]; + serializedPaginations: SerializedPagination[]; pageFilters: any; pageSorters: any; + getLayout: (name?: string, fallback?: string) => string | undefined; } diff --git a/src/pagination.ts b/src/pagination.ts index dad7442..2413b60 100644 --- a/src/pagination.ts +++ b/src/pagination.ts @@ -1,84 +1,66 @@ import { AppContext } from './interface/VuePress' -import { InternalPagination } from './interface/Pagination' +import { + InternalPagination, + PageFilter, + GetPaginationPageUrl, + getPaginationPageTitle, + SerializedPagination, +} from './interface/Pagination' import { logPages } from './util' -export async function registerPagination(paginations: InternalPagination[], ctx: AppContext) { - ctx.paginations = [] - ctx.pageFilters = {} - ctx.pageSorters = {} +export async function registerPagination( + paginations: InternalPagination[], + ctx: AppContext, +) { + ctx.serializedPaginations = [] + ctx.pageFilters = [] + ctx.pageSorters = [] function recordPageFilters(pid, filter) { - if (ctx.pageFilters[pid]) { - return - } + if (ctx.pageFilters[pid]) return ctx.pageFilters[pid] = filter.toString() } function recordPageSorters(pid, sorter) { - if (ctx.pageSorters[pid]) { - return - } + if (ctx.pageSorters[pid]) return ctx.pageSorters[pid] = sorter.toString() } for (const { pid, id, - meta, - getUrl = index => `/${id}/${index}/`, - getTitle = index => `Page ${index + 1} | ${id}`, - options, + filter, + sorter, + layout, + lengthPerPage, + getPaginationPageUrl, + getPaginationPageTitle, } of paginations) { - const defaultPostsFilterMeta = { - args: ['page'], - body: `return page.pid === ${JSON.stringify( - pid, - )} && page.id === ${JSON.stringify(id)}`, - } - - const defaultPostsFilter = new Function( - // @ts-ignore - defaultPostsFilterMeta.args, - defaultPostsFilterMeta.body, - ) - const defaultPostsSorter = (prev, next) => { - const prevTime = new Date(prev.frontmatter.date).getTime() - const nextTime = new Date(next.frontmatter.date).getTime() - return prevTime - nextTime > 0 ? -1 : 1 - } - - const { - perPagePosts = 10, - layout = 'Layout', - serverPageFilter = defaultPostsFilter, - clientPageFilter = defaultPostsFilter, - // @ts-ignore - clientPageSorter = defaultPostsSorter, - } = options - const { pages: sourcePages } = ctx - const pages = sourcePages.filter(serverPageFilter) + const pages = sourcePages.filter(filter as PageFilter) - const intervallers = getIntervallers(pages.length, perPagePosts) - const pagination = { + const intervallers = getIntervallers(pages.length, lengthPerPage) + const pagination: SerializedPagination = { pid, id, - paginationPages: intervallers.map((interval, index) => { - const path = getUrl(index) + filter: `filters.${pid}`, + sorter: `sorters.${pid}`, + pages: intervallers.map((interval, index) => { + const path = (getPaginationPageUrl as GetPaginationPageUrl)(index) return { path, interval } }), } - recordPageFilters(pid, clientPageFilter) - recordPageSorters(pid, clientPageSorter) + recordPageFilters(pid, filter) + recordPageSorters(pid, sorter) logPages( - `Automatically Added Pagination Pages`, - pagination.paginationPages.slice(1) + `Automatically generated pagination pages`, + pagination.pages.slice(1), ) await Promise.all( - pagination.paginationPages.map(async ({ path }, index) => { + pagination.pages.map(async ({ path }, index) => { if (index === 0) { return } @@ -87,17 +69,27 @@ export async function registerPagination(paginations: InternalPagination[], ctx: permalink: path, frontmatter: { layout, - title: getTitle(index), + title: (getPaginationPageTitle as getPaginationPageTitle)(index), + }, + meta: { + pid, + id, }, - meta, }) }), ) // @ts-ignore - ctx.paginations.push(pagination) + ctx.serializedPaginations.push(pagination) } } +/** + * Divided an interval of several lengths into several equal-length intervals. + * + * @param max + * @param interval + */ + function getIntervallers(max, interval) { const count = max % interval === 0 diff --git a/src/util.ts b/src/util.ts index 61be647..fbfbef2 100644 --- a/src/util.ts +++ b/src/util.ts @@ -1,6 +1,10 @@ import { env } from '@vuepress/shared-utils' +import { AppContext, Page } from './interface/VuePress' +import { ClassifierTypeEnum, DefaultLayoutEnum } from './interface/Classifier' +import { PaginationConfig } from './interface/Pagination' export type FrontmatterHandler = (key: string, pageKey: string) => void + export interface FrontmatterTempMap { scope: string; path: string; @@ -31,21 +35,11 @@ export function logPages(title, pages) { const chalk = require('chalk') console.log() console.log(chalk.cyan(`[@vuepress/plugin-blog] ====== ${title} ======`)) - const data: any[] = [ - ['permalink', 'meta', 'pid', 'id', 'frontmatter'], - ] + const data: any[] = [['permalink', 'meta', 'pid', 'id', 'frontmatter']] data.push( - ...pages.map(({ - // @ts-ignore - path, - permalink, - meta, - // @ts-ignore - pid, - // @ts-ignore - id, - frontmatter, - }) => [ + ...pages.map(({ // @ts-ignore + path, permalink, meta, pid, id, frontmatter }) => [ + // @ts-ignore // @ts-ignore permalink || path || '', JSON.stringify(meta) || '', pid || '', @@ -57,3 +51,72 @@ export function logPages(title, pages) { console.log() } } + +export function resolvePaginationConfig( + classifierType: ClassifierTypeEnum, + pagination = {} as PaginationConfig, + indexPath, + pid, // post / tag + id, // post / js + ctx: AppContext, + keys: string[] = [''], // ['js'] +) { + return Object.assign( + {}, + { + lengthPerPage: 10, + layout: ctx.getLayout(DefaultLayoutEnum.DirectoryPagination), + + getPaginationPageUrl(index) { + if (index === 0) { + return indexPath + } + return `${indexPath}page/${index + 1}/` + }, + + filter: + classifierType === ClassifierTypeEnum.Directory + ? getIdentityFilter(pid, id) + : getFrontmatterClassifierPageFilter(keys, id), + + sorter: (prev: Page, next: Page) => { + const prevTime = new Date(prev.frontmatter.date).getTime() + const nextTime = new Date(next.frontmatter.date).getTime() + return prevTime - nextTime > 0 ? -1 : 1 + }, + }, + pagination, + ) +} + +function getIdentityFilter(pid, id) { + return new Function( + // @ts-ignore + ['page'], + `return page.pid === ${JSON.stringify(pid)} && page.id === ${JSON.stringify( + id, + )}`, + ) +} + +function getFrontmatterClassifierPageFilter(keys, value) { + return new Function( + // @ts-ignore + ['page'], + ` +const keys = ${JSON.stringify(keys)}; +const value = ${JSON.stringify(value)}; +return keys.some(key => { + const _value = page.frontmatter[key] + if (Array.isArray(_value)) { + return _value.some(i => i === value) + } + return _value === value +}) + `, + ) +} + +export function UpperFirstChar(str) { + return str.charAt(0).toUpperCase() + str.slice(1) +}