diff --git a/packages/@vuepress/core/lib/app/Store.js b/packages/@vuepress/core/lib/app/Store.js new file mode 100644 index 0000000000..cf3e5b60b9 --- /dev/null +++ b/packages/@vuepress/core/lib/app/Store.js @@ -0,0 +1,19 @@ +import Vue from 'vue' + +export default class Store { + constructor () { + this.store = new Vue({ + data: { + ob: {} + } + }) + } + + get (key) { + return this.store.ob[key] + } + + set (key, value) { + Vue.set(this.store.ob, key, value) + } +} diff --git a/packages/@vuepress/core/lib/app/app.js b/packages/@vuepress/core/lib/app/app.js index 37afbdfee4..171cfd2929 100644 --- a/packages/@vuepress/core/lib/app/app.js +++ b/packages/@vuepress/core/lib/app/app.js @@ -6,7 +6,8 @@ import { routes } from '@internal/routes' import { siteData } from '@internal/siteData' import appEnhancers from '@internal/app-enhancers' import globalUIComponents from '@internal/global-ui' -import I18n from '@internal/i18n' +import ClientComputedMixin from '../prepare/ClientComputedMixin' +import Store from './Store' // generated from user config import('@temp/style.styl') @@ -30,9 +31,12 @@ if (module.hot) { } Vue.config.productionTip = false + +Vue.$store = new Store() + Vue.use(Router) // mixin for exposing $site and $page -Vue.mixin(dataMixin(I18n, siteData)) +Vue.mixin(dataMixin(ClientComputedMixin, siteData)) // component for rendering markdown content and setting title etc. Vue.component('Content', Content) Vue.component('OutboundLink', OutboundLink) @@ -59,7 +63,7 @@ export function createApp (isServer) { if (saved) { return saved } else if (to.hash) { - if (Vue.$store.disableScrollBehavior) { + if (Vue.$store.get('disableScrollBehavior')) { return false } return { diff --git a/packages/@vuepress/core/lib/app/dataMixin.js b/packages/@vuepress/core/lib/app/dataMixin.js index d52115fc2a..1f0b2dfa18 100644 --- a/packages/@vuepress/core/lib/app/dataMixin.js +++ b/packages/@vuepress/core/lib/app/dataMixin.js @@ -4,22 +4,16 @@ import Vue from 'vue' export default function dataMixin (I18n, siteData) { prepare(siteData) - const store = new Vue({ - data: { - siteData, - disableScrollBehavior: false - } - }) - Vue.$store = store + Vue.$store.set('siteData', siteData) if (module.hot) { module.hot.accept(VUEPRESS_TEMP_PATH + '/internal/siteData.js', () => { prepare(siteData) - store.siteData = siteData + Vue.$store.set('siteData', siteData) }) } - const I18nConstructor = I18n(store) + const I18nConstructor = I18n(Vue.$store.get('siteData')) const i18n = new I18nConstructor() const descriptors = Object.getOwnPropertyDescriptors(Object.getPrototypeOf(i18n)) const computed = {} diff --git a/packages/@vuepress/core/lib/internal-plugins/i18nTemp.js b/packages/@vuepress/core/lib/internal-plugins/i18nTemp.js deleted file mode 100644 index 52209b0c9c..0000000000 --- a/packages/@vuepress/core/lib/internal-plugins/i18nTemp.js +++ /dev/null @@ -1,18 +0,0 @@ -const path = require('path') -const { fs } = require('@vuepress/shared-utils') - -// This plugin generate a mirror es module of 'lib/prepare/I18n.js' -// Since for now Webpack doesn't allow 'ES6 import' to import a a commonjs -// module. -// -// Ref: https://github.com/webpack/webpack/issues/4039 -module.exports = () => ({ - name: '@vuepress/internal-i18n-temp', - - // @internal/i18n - async clientDynamicModules () { - const i18nCommonjsModule = await fs.readFile(path.resolve(__dirname, '../prepare/I18n.js'), 'utf-8') - const i18nEsModule = i18nCommonjsModule.replace('module.exports =', 'export default') - return { name: 'i18n.js', content: i18nEsModule, dirname: 'internal' } - } -}) diff --git a/packages/@vuepress/core/lib/prepare/AppContext.js b/packages/@vuepress/core/lib/prepare/AppContext.js index fc00e69d10..c5be48465c 100644 --- a/packages/@vuepress/core/lib/prepare/AppContext.js +++ b/packages/@vuepress/core/lib/prepare/AppContext.js @@ -11,7 +11,7 @@ const loadTheme = require('./loadTheme') const { fs, logger, chalk, globby, sort, datatypes: { isFunction }} = require('@vuepress/shared-utils') const Page = require('./Page') -const I18n = require('./I18n') +const ClientComputedMixin = require('./ClientComputedMixin') const PluginAPI = require('../plugin-api/index') /** @@ -54,7 +54,7 @@ module.exports = class AppContext { this.pluginAPI = new PluginAPI(this) this.pages = [] // Array - this.I18nConstructor = I18n(null) + this.ClientComputedMixinConstructor = ClientComputedMixin(this.getSiteData()) } /** @@ -73,8 +73,8 @@ module.exports = class AppContext { await this.resolvePages() await Promise.all( - this.pluginAPI.options.additionalPages.values.map(async ({ path, permalink }) => { - await this.addPage({ filePath: path, permalink }) + this.pluginAPI.options.additionalPages.values.map(async (options) => { + await this.addPage(options) }) ) @@ -109,7 +109,6 @@ module.exports = class AppContext { .use(require('../internal-plugins/rootMixins')) .use(require('../internal-plugins/enhanceApp')) .use(require('../internal-plugins/overrideCSS')) - .use(require('../internal-plugins/i18nTemp')) .use(require('../internal-plugins/layoutComponents')) .use(require('../internal-plugins/pageComponents')) // user plugin @@ -213,10 +212,10 @@ module.exports = class AppContext { async addPage (options) { options.permalinkPattern = this.siteConfig.permalink - const page = new Page(options) + const page = new Page(options, this) await page.process({ markdown: this.markdown, - i18n: new this.I18nConstructor((this.getSiteData.bind(this))), + computed: new this.ClientComputedMixinConstructor(), enhancers: this.pluginAPI.options.extendPageData.items }) this.pages.push(page) @@ -249,13 +248,20 @@ module.exports = class AppContext { */ getSiteData () { + const { locales } = this.siteConfig + if (locales) { + Object.keys(locales).forEach(path => { + locales[path].path = path + }) + } + return { title: this.siteConfig.title || '', description: this.siteConfig.description || '', base: this.base, pages: this.pages.map(page => page.toJson()), themeConfig: this.siteConfig.themeConfig || {}, - locales: this.siteConfig.locales + locales } } } diff --git a/packages/@vuepress/core/lib/prepare/ClientComputedMixin.js b/packages/@vuepress/core/lib/prepare/ClientComputedMixin.js new file mode 100644 index 0000000000..5922819432 --- /dev/null +++ b/packages/@vuepress/core/lib/prepare/ClientComputedMixin.js @@ -0,0 +1,128 @@ +'use strict' + +/** + * Get page data via path (permalink). + * + * @param {array} pages + * @param {string} path + * @returns {object} + */ + +function findPageForPath (pages, path) { + for (let i = 0; i < pages.length; i++) { + const page = pages[i] + if (page.path === path) { + return page + } + } + return { + path: '', + frontmatter: {} + } +} + +/** + * Expose a function to get ClientComputedMixin constructor. + * Note that this file will run in both server and client side. + * + * @param {object} siteData + * @returns {ClientComputedMixin} + */ + +module.exports = siteData => { + // We cannot use class here. webpack will throw error. + function ClientComputedMixin () {} + + ClientComputedMixin.prototype = { + setPage (page) { + this.__page = page + }, + + get $site () { + return siteData + }, + + get $themeConfig () { + return this.$site.themeConfig + }, + + get $localeConfig () { + const { locales = {}} = this.$site + let targetLang + let defaultLang + for (const path in locales) { + if (path === '/') { + defaultLang = locales[path] + } else if (this.$page.path.indexOf(path) === 0) { + targetLang = locales[path] + } + } + return targetLang || defaultLang || {} + }, + + get $siteTitle () { + return this.$localeConfig.title || this.$site.title || '' + }, + + get $title () { + const page = this.$page + const siteTitle = this.$siteTitle + const selfTitle = page.frontmatter.home ? null : ( + page.frontmatter.title || // explicit title + page.title // inferred title + ) + return siteTitle + ? selfTitle + ? (selfTitle + ' | ' + siteTitle) + : siteTitle + : selfTitle || 'VuePress' + }, + + get $description () { + // #565 hoist description from meta + const description = getMetaDescription(this.$page.frontmatter.meta) + if (description) { + return description + } + // if (this.$page.frontmatter.meta) { + // const descriptionMeta = this.$page.frontmatter.meta.filter(item => item.name === 'description')[0] + // if (descriptionMeta) return descriptionMeta.content + // } + return this.$page.frontmatter.description || this.$localeConfig.description || this.$site.description || '' + }, + + get $lang () { + return this.$page.frontmatter.lang || this.$localeConfig.lang || 'en-US' + }, + + get $localePath () { + return this.$localeConfig.path || '/' + }, + + get $themeLocaleConfig () { + return (this.$site.themeConfig.locales || {})[this.$localePath] || {} + }, + + get $page () { + if (this.__page) { + return this.__page + } + return findPageForPath( + this.$site.pages, + this.$route.path + ) + } + } + + return ClientComputedMixin +} + +function getMetaDescription (meta) { + if (meta) { + // Why '(() => 'name')()' ? + // You can use item.name directly and see what happened. + // "How many pits did webpack bury?" + const descriptionMeta = meta.filter(item => item[(() => 'name')()] === 'description')[0] + if (descriptionMeta) return descriptionMeta.content + } +} diff --git a/packages/@vuepress/core/lib/prepare/I18n.js b/packages/@vuepress/core/lib/prepare/I18n.js deleted file mode 100644 index 8176c592b8..0000000000 --- a/packages/@vuepress/core/lib/prepare/I18n.js +++ /dev/null @@ -1,121 +0,0 @@ -'use strict' - -/** - * Get page data via path (permalink). - * - * @param {array} pages - * @param {string} path - * @returns {object} - */ - -function findPageForPath (pages, path) { - for (let i = 0; i < pages.length; i++) { - const page = pages[i] - if (page.path === path) { - return page - } - } - return { - path: '', - frontmatter: {} - } -} - -/** - * Expose I18n constructor.Note that this file will - * be run in both server and client side. - * - * @param store - * @returns {{new(*): I18n, prototype: I18n}} - */ - -module.exports = (store /* null in server side */) => class I18n { - constructor (dataProvider) { - this.__ssrContext = true - this.__dataProvider = dataProvider - } - - setSSRContext (context) { - this.__ssrContext = context - } - - get $site () { - if (this.__ssrContext) { - const siteData = this.__dataProvider() - if (siteData.locales) { - Object.keys(siteData.locales).forEach(path => { - siteData.locales[path].path = path - }) - } - return siteData - } - return store.siteData - } - - get $themeConfig () { - return this.$site.themeConfig - } - - get $localeConfig () { - const { locales = {}} = this.$site - let targetLang - let defaultLang - for (const path in locales) { - if (path === '/') { - defaultLang = locales[path] - } else if (this.$page.path.indexOf(path) === 0) { - targetLang = locales[path] - } - } - return targetLang || defaultLang || {} - } - - get $siteTitle () { - return this.$localeConfig.title || this.$site.title || '' - } - - get $title () { - const page = this.$page - const siteTitle = this.$siteTitle - const selfTitle = page.frontmatter.home ? null : ( - page.frontmatter.title || // explicit title - page.title // inferred title - ) - return siteTitle - ? selfTitle - ? (selfTitle + ' | ' + siteTitle) - : siteTitle - : selfTitle || 'VuePress' - } - - get $description () { - // #565 hoist description from meta - if (this.$page.frontmatter.meta) { - const descriptionMeta = this.$page.frontmatter.meta.filter(item => item.name === 'description')[0] - if (descriptionMeta) return descriptionMeta.content - } - return this.$page.frontmatter.description || this.$localeConfig.description || this.$site.description || '' - } - - get $lang () { - return this.$page.frontmatter.lang || this.$localeConfig.lang || 'en-US' - } - - get $localePath () { - return this.$localeConfig.path || '/' - } - - get $themeLocaleConfig () { - return (this.$site.themeConfig.locales || {})[this.$localePath] || {} - } - - get $page () { - if (this.__ssrContext) { - return this.__ssrContext - } - return findPageForPath( - this.$site.pages, - this.$route.path - ) - } -} diff --git a/packages/@vuepress/core/lib/prepare/Page.js b/packages/@vuepress/core/lib/prepare/Page.js index 5f30d0de24..ed72e94256 100644 --- a/packages/@vuepress/core/lib/prepare/Page.js +++ b/packages/@vuepress/core/lib/prepare/Page.js @@ -6,7 +6,7 @@ const path = require('path') const slugify = require('../markdown/slugify') -const { inferTitle, extractHeaders } = require('../util/index') +const { inferTitle, inferDate, extractHeaders, DATE_RE } = require('../util/index') const { fs, fileToPath, parseFrontmatter, getPermalink } = require('@vuepress/shared-utils') /** @@ -24,6 +24,7 @@ module.exports = class Page { * @param {object} frontmatter * @param {string} permalinkPattern */ + constructor ({ path, meta, @@ -41,18 +42,19 @@ module.exports = class Page { this._content = content this._permalink = permalink this.frontmatter = frontmatter + this._permalinkPattern = permalinkPattern if (relative) { - this._routePath = encodeURI(fileToPath(relative)) + this.regularPath = encodeURI(fileToPath(relative)) } else if (path) { - this._routePath = encodeURI(path) + this.regularPath = encodeURI(path) } else if (permalink) { - this._routePath = encodeURI(permalink) + this.regularPath = encodeURI(permalink) } this.key = 'v-' + Math.random().toString(16).slice(2) - this.regularPath = this.path = this._routePath - this._permalinkPattern = permalinkPattern + // Using regularPath first, would be override by permalink later. + this.path = this.regularPath } /** @@ -66,9 +68,9 @@ module.exports = class Page { */ async process ({ - i18n, + computed, markdown, - enhancers + enhancers = [] }) { if (this._filePath) { this._content = await fs.readFile(this._filePath, 'utf-8') @@ -102,9 +104,9 @@ module.exports = class Page { } // resolve i18n - i18n.setSSRContext(this) - this._i18n = i18n - this._localePath = i18n.$localePath + computed.setPage(this) + this._computed = computed + this._localePath = computed.$localePath this.enhance(enhancers) this.buildPermalink() @@ -129,7 +131,33 @@ module.exports = class Page { */ get slug () { - return slugify(this.filename) + return slugify(this.strippedFilename) + } + + /** + * stripped file name. + * + * If filename was yyyy-MM-dd-[title], the date prefix will be stripped. + * If filename was yyyy-MM-[title], the date prefix will be stripped. + * + * @returns {string} + * @api public + */ + + get strippedFilename () { + const match = this.filename.match(DATE_RE) + return match ? match[3] : this.filename + } + + /** + * get date of a page. + * + * @returns {null|string} + * @api public + */ + + get date () { + return inferDate(this.frontmatter, this.filename) } /** @@ -162,7 +190,7 @@ module.exports = class Page { this._permalink = getPermalink({ pattern: this.frontmatter.permalink || this._permalinkPattern, slug: this.slug, - date: this.frontmatter.date, + date: this.date, localePath: this._localePath, regularPath: this.regularPath }) diff --git a/packages/@vuepress/core/lib/util/index.js b/packages/@vuepress/core/lib/util/index.js index 4d682728f9..04f70eaacb 100644 --- a/packages/@vuepress/core/lib/util/index.js +++ b/packages/@vuepress/core/lib/util/index.js @@ -70,9 +70,6 @@ exports.inferTitle = function (frontmatter, strippedContent) { } } -const LRU = require('lru-cache') -const cache = LRU({ max: 1000 }) - /** * Extract heeaders from markdown source content. * @@ -82,6 +79,9 @@ const cache = LRU({ max: 1000 }) * @returns {array} */ +const LRU = require('lru-cache') +const cache = LRU({ max: 1000 }) + exports.extractHeaders = function (content, include = [], md) { const key = content + include.join(',') const hit = cache.get(key) @@ -107,3 +107,25 @@ exports.extractHeaders = function (content, include = [], md) { cache.set(key, res) return res } + +/** + * Infer date. + * + * @param {object} frontmatter + * @param {string} filename + * @returns {null|string} + */ + +const DATE_RE = /(\d{4}-\d{1,2}(-\d{1,2})?)-(.*)/ +exports.DATE_RE = DATE_RE + +exports.inferDate = function (frontmatter = {}, filename) { + if (frontmatter.date) { + return frontmatter.date + } + const match = filename.match(DATE_RE) + if (match) { + return match[1] + } + return null +} diff --git a/packages/@vuepress/plugin-active-header-links/mixin.js b/packages/@vuepress/plugin-active-header-links/mixin.js index 50658fa65c..4692f76afe 100644 --- a/packages/@vuepress/plugin-active-header-links/mixin.js +++ b/packages/@vuepress/plugin-active-header-links/mixin.js @@ -31,11 +31,11 @@ export default { (!nextAnchor || scrollTop < nextAnchor.parentElement.offsetTop - 10)) if (isActive && decodeURIComponent(this.$route.hash) !== decodeURIComponent(anchor.hash)) { - Vue.$store.disableScrollBehavior = true + Vue.$store.set('disableScrollBehavior', true) this.$router.replace(decodeURIComponent(anchor.hash), () => { // execute after scrollBehavior handler. this.$nextTick(() => { - Vue.$store.disableScrollBehavior = false + Vue.$store.set('disableScrollBehavior', false) }) }) return