feat: refine blog plugin

This commit is contained in:
ULIVZ
2019-01-16 00:40:25 +08:00
parent 3b28fdb382
commit e29fbeaecb
11 changed files with 533 additions and 229 deletions
@@ -291,6 +291,9 @@ module.exports = class AppContext {
*/
async addPage (options) {
if (this.pageExists(options)) {
console.log(`Page already exists`, options)
}
options.permalinkPattern = this.siteConfig.permalink
const page = new Page(options, this)
await page.process({
@@ -301,6 +304,10 @@ module.exports = class AppContext {
this.pages.push(page)
}
pageExists ({ permalink }) {
return this.pages.includes(page => page.permalink === permalink)
}
/**
* Resolve theme
*
+4 -1
View File
@@ -45,7 +45,8 @@ module.exports = class Page {
relative,
permalink,
frontmatter = {},
permalinkPattern
permalinkPattern,
...rest
}, context) {
this.title = title
this._meta = meta
@@ -56,6 +57,8 @@ module.exports = class Page {
this._permalinkPattern = permalinkPattern
this._context = context
Object.assign(this, rest)
if (relative) {
this.regularPath = encodeURI(fileToPath(relative))
} else if (path) {
@@ -1,6 +1,5 @@
import { findPageByKey } from '@app/util'
import tagMeta from '@dynamic/tag'
import categoryMeta from '@dynamic/category'
import frontmatterClassifiedPageMap from '@dynamic/vuepress_blog/frontmatterClassified'
class Classifiable {
constructor (metaMap, pages) {
@@ -38,26 +37,28 @@ class Classifiable {
}
export default ({ Vue }) => {
Vue.mixin({
computed: {
$tags () {
const { pages } = this.$site
const tags = new Classifiable(tagMeta, pages)
return tags
},
$tag () {
const tagName = this.$route.meta.tagName
return this.$tags.getItemByName(tagName)
},
$categories () {
const { pages } = this.$site
const categories = new Classifiable(categoryMeta, pages)
return categories
},
$category () {
const categoryName = this.$route.meta.categoryName
return this.$categories.getItemByName(categoryName)
const computed = Object.keys(frontmatterClassifiedPageMap)
.map(classifiedType => {
const map = frontmatterClassifiedPageMap[classifiedType]
const helperName = `$${classifiedType}`
return {
[helperName] () {
const { pages } = this.$site
const classified = new Classifiable(map, pages)
return classified
},
[`$current${classifiedType.charAt(0).toUpperCase() + classifiedType.slice(1)}`] () {
const tagName = this.$route.meta.frontmatterClassificationKey
return this[helperName].getItemByName(tagName)
}
}
}
})
.reduce((map, item) => {
Object.assign(map, item)
return map
}, {})
Vue.mixin({
computed
})
}
@@ -0,0 +1,110 @@
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
}
get pages () {
return Vue.$vuepress.$get('siteData').pages
}
getPagination (pid, id, route) {
debug('id', id)
debug('this.paginations', this.paginations)
const pagnination = this.paginations.filter(p => p.id === id && p.pid === pid)[0]
return new Pagination(pagnination, this.pages, route)
}
}
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 { path } = route
for (let i = 0, l = paginationPages.length; i < l; i++) {
const page = paginationPages[i]
if (page.path === path) {
this.paginationIndex = i
break
}
}
if (!this.paginationIndex) {
this.paginationIndex = 0
}
this._paginationPages = paginationPages
this._currentPage = paginationPages[this.paginationIndex]
this._matchedPages = pages.filter(pageFilter).sort(pageSorter)
}
setIndexPage (path) {
this._indexPage = path
}
get length () {
return this._paginationPages.length
}
get pages () {
const [start, end] = this._currentPage.interval
return this._matchedPages.slice(start, end + 1)
}
get hasPrev () {
return this.paginationIndex !== 0
}
get prevLink () {
if (this.hasPrev) {
if (this.paginationIndex - 1 === 0 && this._indexPage) {
return this._indexPage
}
return this._paginationPages[this.paginationIndex - 1].path
}
}
get hasNext () {
return this.paginationIndex !== this.length - 1
}
get nextLink () {
if (this.hasNext) {
return this._paginationPages[this.paginationIndex + 1].path
}
}
}
export default ({ Vue }) => {
Vue.mixin({
methods: {
$pagination (pid, id) {
id = id || pid
return gateway.getPagination(pid, id, this.$route)
}
}
})
}
+26
View File
@@ -0,0 +1,26 @@
export interface Pagination {
postsFilter?: typeof Array.prototype.filter
postsSorter?: typeof Array.prototype.sort
perPagePosts?: number
layout?: string
}
export interface DirectoryClassification {
id: string;
dirname: string;
path: string;
layout?: string;
itemLayout?: string;
itemPermalink?: string;
pagination?: Pagination;
}
export interface FrontmatterClassification {
id: string;
keys: string[];
path: string;
layout?: string;
frontmatter?: Record<string, any>;
itemlayout?: string;
pagination?: Pagination;
}
+135 -135
View File
@@ -1,81 +1,23 @@
const { path, datatypes: { isString }} = require('@vuepress/shared-utils')
const { path } = require('@vuepress/shared-utils')
const { curryFrontmatterHandler } = require('./lib/util')
const { handleOptions } = require('./lib/handleOptions')
const { registerPagination } = require('./lib/pagination')
module.exports = (options, ctx) => {
const { layoutComponentMap } = ctx
const {
pageEnhancers = [],
postsDir = '_posts',
archivePostsDir = '_archive',
archivePageUrl = '/archive/',
categoryIndexPageUrl = '/category/',
tagIndexPageUrl = '/tag/',
permalink = '/:year/:month/:day/:slug'
} = options
const isLayoutExists = name => layoutComponentMap[name] !== undefined
const getLayout = (name, fallback) => {
return isLayoutExists(name) ? name : (fallback || 'Layout')
}
const isDirectChild = regularPath => path.parse(regularPath).dir === '/'
const enhancers = [
{
when: ({ regularPath }) => regularPath === categoryIndexPageUrl,
frontmatter: { layout: getLayout('Categories', 'Page') }
},
{
when: ({ regularPath }) => regularPath.startsWith('/category/'),
frontmatter: { layout: getLayout('Category', 'Page') }
},
{
when: ({ regularPath }) => regularPath === tagIndexPageUrl,
frontmatter: { layout: getLayout('Tags', 'Page') }
},
{
when: ({ regularPath }) => regularPath.startsWith('/tag/'),
frontmatter: { layout: getLayout('Tag', 'Page') }
},
{
when: ({ regularPath }) => regularPath === '/',
frontmatter: { layout: getLayout('Layout') }
},
{
when: ({ regularPath }) => regularPath.startsWith(`/${postsDir}/`),
frontmatter: {
layout: getLayout('Post', 'Page'),
permalink
},
data: { type: 'post' }
},
{
when: ({ regularPath }) => regularPath === archivePageUrl,
frontmatter: { layout: getLayout('Archive', 'Page') }
},
{
when: ({ regularPath }) => regularPath.startsWith(`/${archivePostsDir}/`),
frontmatter: {
layout: getLayout('Post', 'Page'),
permalink: `/archive/${permalink}`
},
data: { type: 'archived_post' }
},
...pageEnhancers,
{
when: ({ regularPath }) => isDirectChild(regularPath),
frontmatter: { layout: getLayout('Page', 'Layout') },
data: { type: 'page' }
}
]
pageEnhancers,
frontmatterClassificationPages,
extraPages,
paginations
} = handleOptions(options, ctx)
return {
/**
* Modify page's metadata according to the habits of blog users.
* Modify page's metadata.
*/
extendPageData (pageCtx) {
const { frontmatter: rawFrontmatter } = pageCtx
enhancers.forEach(({
pageEnhancers.forEach(({
when,
data = {},
frontmatter = {}
@@ -97,92 +39,150 @@ module.exports = (options, ctx) => {
*/
async ready () {
const { pages } = ctx
const tagMap = {}
const categoryMap = {}
const curryHandler = (scope, map) => (key, pageKey) => {
if (key) {
if (!map[key]) {
map[key] = {}
map[key].path = `/${scope}/${key}.html`
map[key].pageKeys = []
}
map[key].pageKeys.push(pageKey)
}
}
const handleTag = curryHandler('tag', tagMap)
const handleCategory = curryHandler('category', categoryMap)
pages.forEach(({
key,
frontmatter: {
tag,
tags,
category,
categories
}
// 1. Tweak data structure to store the classified info.
const frontmatterClassifiedPages = frontmatterClassificationPages.map(({
id,
keys,
pagination
}) => {
if (isString(tag)) {
handleTag(tag, key)
}
if (Array.isArray(tags)) {
tags.forEach(tag => handleTag(tag, key))
}
if (isString(category)) {
handleCategory(category, key)
}
if (Array.isArray(categories)) {
categories.forEach(category => handleCategory(category, key))
const map = {}
return {
id,
keys,
pagination,
map,
_handler: curryFrontmatterHandler(id, map)
}
})
ctx.tagMap = tagMap
ctx.categoryMap = categoryMap
// 2. Handle frontmatter per page.
for (const { key: pageKey, frontmatter } of pages) {
if (!frontmatter || Object.keys(frontmatter).length === 0) {
continue
}
for (const { keys, _handler } of frontmatterClassifiedPages) {
for (const key of keys) {
const fieldValue = frontmatter[key]
Array.isArray(fieldValue)
? fieldValue.forEach(v => _handler(v, pageKey))
: _handler(fieldValue, pageKey)
}
}
}
const extraPages = [
{
permalink: tagIndexPageUrl,
frontmatter: { title: `Tags` }
},
{
permalink: categoryIndexPageUrl,
frontmatter: { title: `Categories` }
},
{
permalink: archivePageUrl,
frontmatter: { title: `Archive` }
},
...Object.keys(tagMap).map(tagName => ({
permalink: tagMap[tagName].path,
meta: { tagName },
frontmatter: { title: `${tagName} | Tag` }
})),
...Object.keys(categoryMap).map(categoryName => ({
permalink: categoryMap[categoryName].path,
meta: { categoryName },
frontmatter: { title: `${categoryName} | Category` }
}))
ctx.frontmatterClassifiedPages = frontmatterClassifiedPages
// 3. Staticize all pages.
const allExtraPages = [
...extraPages,
...frontmatterClassifiedPages
.map(frontmatterClassifiedPage => {
const { map, pagination, keys } = frontmatterClassifiedPage
return Object.keys(map).map((key) => {
const { path, scope } = map[key]
// Register pagination
paginations.push({
pid: scope,
id: key,
options: {
...pagination,
layout: 'FrontmatterClassification',
serverPageFilter (page) {
return clientFrontmatterClassificationPageFilter(page, keys, key)
},
clientPageFilter: clientFrontmatterClassificationPageFilter
},
getUrl (index) {
if (index === 0) {
return `/${scope}/${key}/`
}
return `/${scope}/${key}/page/${index + 1}/`
},
getTitle (index) {
return `Page ${index + 1} - ${key} | ${scope}`
}
})
return {
permalink: path,
meta: {
frontmatterClassificationKey: scope
},
pid: scope,
id: key,
frontmatter: {
layout: 'FrontmatterClassification',
title: `${key} | ${scope}`
}
}
})
})
.reduce((arr, next) => arr.concat(next), [])
]
await Promise.all(extraPages.map(page => ctx.addPage(page)))
await Promise.all(allExtraPages.map(async page => ctx.addPage(page)))
await registerPagination(paginations, ctx)
},
/**
* Generate tag and category metadata.
*/
async clientDynamicModules () {
const frontmatterClassifiedPageMap = ctx.frontmatterClassifiedPages
.reduce((map, page) => {
map[page.id] = page.map
return map
}, {})
const PREFIX = 'vuepress_blog'
return [
{
name: 'tag.js',
content: `export default ${JSON.stringify(ctx.tagMap, null, 2)}`
name: `${PREFIX}/frontmatterClassifications.js`,
content: `export default ${JSON.stringify(frontmatterClassificationPages, null, 2)}`
},
{
name: 'category.js',
content: `export default ${JSON.stringify(ctx.categoryMap, null, 2)}`
name: `${PREFIX}/frontmatterClassified.js`,
content: `export default ${JSON.stringify(frontmatterClassifiedPageMap, null, 2)}`
},
{
name: `${PREFIX}/paginations.js`,
content: `export default ${JSON.stringify(ctx.paginations, null, 2)}`
},
{
name: `${PREFIX}/pageFilters.js`,
content: `export default ${mapToString(ctx.pageFilters)}`
},
{
name: `${PREFIX}/pageSorters.js`,
content: `export default ${mapToString(ctx.pageSorters)}`
}
]
},
enhanceAppFiles: path.resolve(__dirname, 'enhanceAppFile.js')
enhanceAppFiles: [
path.resolve(__dirname, 'client/classification.js'),
path.resolve(__dirname, 'client/pagination.js')
]
}
}
function mapToString (map) {
let str = `{\n`
for (const key of Object.keys(map)) {
str += ` "${key}": ${map[key]},\n`
}
str += '}'
return str
}
function clientFrontmatterClassificationPageFilter (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
})
}
@@ -0,0 +1,119 @@
function handleOptions (options, ctx) {
const { layoutComponentMap } = ctx
const {
directories = [],
frontmatters = []
} = options
const isLayoutExists = name => layoutComponentMap[name] !== undefined
const getLayout = (name, fallback) => {
return isLayoutExists(name) ? name : (fallback || 'Layout')
}
const pageEnhancers = []
const frontmatterClassificationPages = []
const extraPages = []
const paginations = []
// 1. Directory-based classification
for (const directory of directories) {
const {
id,
dirname,
path: listPath,
layout: listLayout,
frontmatter,
itemLayout,
itemPermalink,
pagination = {
perPagePosts: 10
}
} = directory
if (!listPath) {
continue
}
extraPages.push({
permalink: listPath,
frontmatter
})
pageEnhancers.push({
when: ({ regularPath }) => regularPath === listPath,
frontmatter: { layout: getLayout(listLayout) }
})
pageEnhancers.push({
when: ({ regularPath }) => regularPath && regularPath !== listPath && regularPath.startsWith(`/${dirname}/`),
frontmatter: {
layout: getLayout(itemLayout),
permalink: itemPermalink
},
data: { id: id, pid: id }
})
paginations.push({
pid: id,
id,
options: pagination,
getUrl (index) {
if (index === 0) {
return listPath
}
return `${listPath}page/${index + 1}/`
}
})
}
// 2. Frontmatter-based classification
for (const frontmatterPage of frontmatters) {
const {
id,
keys,
path: listPath,
layout: listLayout,
frontmatter,
itemLayout,
pagination = {
perPagePosts: 10
}
} = frontmatterPage
if (!listPath) {
continue
}
extraPages.push({
permalink: listPath,
frontmatter
})
frontmatterClassificationPages.push({
id,
pagination,
keys
})
pageEnhancers.push({
when: ({ regularPath }) => regularPath === listPath,
frontmatter: { layout: getLayout(listLayout) }
})
pageEnhancers.push({
when: ({ regularPath }) => regularPath && regularPath !== listPath && regularPath.startsWith(listPath),
frontmatter: { layout: getLayout(itemLayout) }
})
}
return {
pageEnhancers,
frontmatterClassificationPages,
extraPages,
paginations
}
}
module.exports = {
handleOptions
}
@@ -0,0 +1,91 @@
async function registerPagination (paginations, ctx) {
ctx.paginations = []
ctx.pageFilters = {}
ctx.pageSorters = {}
function recordPageFilters (pid, filter) {
if (ctx.pageFilters[pid]) {
return
}
ctx.pageFilters[pid] = filter.toString()
}
function recordPageSorters (pid, sorter) {
if (ctx.pageSorters[pid]) {
return
}
ctx.pageSorters[pid] = sorter.toString()
}
for (const {
pid,
id,
getUrl = (index => `/${id}/${index}/`),
getTitle = (index => `Page ${index + 1} | ${id}`),
options
} of paginations) {
const defaultPostsFilterMeta = {
args: ['page'],
body: `return page.pid === ${JSON.stringify(pid)} && page.id === ${JSON.stringify(id)}`
}
const defaultPostsFilter = new Function(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,
clientPageSorter = defaultPostsSorter
} = options
const { pages: sourcePages } = ctx
const pages = sourcePages.filter(serverPageFilter)
const intervallers = getIntervallers(pages.length, perPagePosts)
const pagination = {
pid,
id,
paginationPages: intervallers.map((interval, index) => {
const path = getUrl(index)
return { path, interval }
})
}
recordPageFilters(pid, clientPageFilter)
recordPageSorters(pid, clientPageSorter)
await Promise.all(pagination.paginationPages.map(async ({ path }, index) => {
if (index === 0) {
return
}
return ctx.addPage({
permalink: path,
frontmatter: {
layout,
title: getTitle(index)
}
})
}))
ctx.paginations.push(pagination)
}
}
function getIntervallers (max, interval) {
const count = max % interval === 0 ? Math.floor(max / interval) : Math.floor(max / interval) + 1
const arr = [...Array(count)]
return arr.map((v, index) => {
const start = index * interval
const end = (index + 1) * interval - 1
return [start, end > max ? max : end]
})
}
module.exports = {
registerPagination
}
@@ -0,0 +1,15 @@
const curryFrontmatterHandler = (scope, map) => (key, pageKey) => {
if (key) {
if (!map[key]) {
map[key] = {}
map[key].scope = scope
map[key].path = `/${scope}/${key}/`
map[key].pageKeys = []
}
map[key].pageKeys.push(pageKey)
}
}
module.exports = {
curryFrontmatterHandler
}
@@ -16,6 +16,9 @@
"vuepress",
"generator"
],
"dependencies": {
"debug": "^4.1.1"
},
"author": "ULIVZ <chl814@foxmail.com>",
"license": "MIT",
"bugs": {
@@ -1,71 +0,0 @@
import paginationMeta from '@dynamic/pagination'
class Pagination {
constructor (pagination, { pages, route }) {
let { postsFilter, postsSorter } = pagination
/* eslint-disable no-eval */
postsFilter = eval(postsFilter)
postsSorter = eval(postsSorter)
const { path } = route
const { paginationPages } = pagination
paginationPages.forEach((page, index) => {
if (page.path === path) {
this.paginationIndex = index
}
})
if (!this.paginationIndex) {
this.paginationIndex = 0
}
this._paginationPages = paginationPages
this._currentPage = paginationPages[this.paginationIndex]
this._posts = pages.filter(postsFilter).sort(postsSorter)
}
get length () {
return this._paginationPages.length
}
get posts () {
const [start, end] = this._currentPage.interval
return this._posts.slice(start, end + 1)
}
get hasPrev () {
return this.paginationIndex !== 0
}
get prevLink () {
if (this.hasPrev) {
return this._paginationPages[this.paginationIndex - 1].path
}
}
get hasNext () {
return this.paginationIndex !== this.length - 1
}
get nextLink () {
if (this.hasNext) {
return this._paginationPages[this.paginationIndex + 1].path
}
}
}
export default ({ Vue }) => {
Vue.mixin({
computed: {
$pagination () {
const { pages } = this.$site
const pagination = new Pagination(paginationMeta, {
pages, route: this.$route
})
return pagination
}
}
})
}