├── src ├── .vuepress │ ├── components │ │ └── .gitkeep │ ├── override.styl │ ├── public │ │ ├── face.jpg │ │ ├── favicon.ico │ │ ├── icons │ │ │ ├── 192.png │ │ │ ├── 512.png │ │ │ └── favicon.png │ │ └── manifest.json │ ├── theme │ │ ├── imgs │ │ │ └── brand.jpg │ │ ├── styles │ │ │ ├── fonts │ │ │ │ └── roboto │ │ │ │ │ ├── Roboto-Bold.eot │ │ │ │ │ ├── Roboto-Bold.ttf │ │ │ │ │ ├── Roboto-Bold.woff │ │ │ │ │ ├── Roboto-Light.eot │ │ │ │ │ ├── Roboto-Light.ttf │ │ │ │ │ ├── Roboto-Thin.eot │ │ │ │ │ ├── Roboto-Thin.ttf │ │ │ │ │ ├── Roboto-Thin.woff │ │ │ │ │ ├── Roboto-Bold.woff2 │ │ │ │ │ ├── Roboto-Light.woff │ │ │ │ │ ├── Roboto-Light.woff2 │ │ │ │ │ ├── Roboto-Medium.eot │ │ │ │ │ ├── Roboto-Medium.ttf │ │ │ │ │ ├── Roboto-Medium.woff │ │ │ │ │ ├── Roboto-Medium.woff2 │ │ │ │ │ ├── Roboto-Regular.eot │ │ │ │ │ ├── Roboto-Regular.ttf │ │ │ │ │ ├── Roboto-Regular.woff │ │ │ │ │ ├── Roboto-Thin.woff2 │ │ │ │ │ └── Roboto-Regular.woff2 │ │ │ ├── theme.styl │ │ │ ├── global.styl │ │ │ ├── config.styl │ │ │ ├── roboto.styl │ │ │ └── content.styl │ │ ├── About.vue │ │ ├── NotFound.vue │ │ ├── libs │ │ │ ├── i18n.js │ │ │ ├── utils.js │ │ │ ├── routes.js │ │ │ ├── socials.js │ │ │ └── blog.js │ │ ├── components │ │ │ ├── Comment.vue │ │ │ ├── PostTime.vue │ │ │ ├── Tag.vue │ │ │ ├── Pagination.vue │ │ │ ├── index.js │ │ │ ├── Share.vue │ │ │ └── PostCard.vue │ │ ├── languages │ │ │ ├── zh-CN.js │ │ │ └── en-US.js │ │ ├── enhanceApp.js │ │ ├── Home.vue │ │ ├── Header.vue │ │ ├── Tags.vue │ │ ├── Post.vue │ │ ├── Footer.vue │ │ ├── SideNav.vue │ │ └── Layout.vue │ ├── ignoreStylus.js │ └── config.js ├── index.md ├── about │ └── index.md ├── tags │ └── index.md └── posts │ ├── text-truncation.md │ ├── test-markdown.md │ ├── webpack-use-lodash.md │ ├── write-good-front-end-component.md │ └── cursor-offset-at-input.md ├── lib ├── noop.js ├── index.js ├── default-theme │ ├── search.svg │ ├── styles │ │ ├── config.styl │ │ ├── arrow.styl │ │ ├── custom-blocks.styl │ │ ├── mobile.styl │ │ ├── nprogress.styl │ │ ├── code.styl │ │ └── theme.styl │ ├── NotFound.vue │ ├── OutboundLink.vue │ ├── DropdownTransition.vue │ ├── NavLink.vue │ ├── SidebarButton.vue │ ├── SidebarGroup.vue │ ├── Navbar.vue │ ├── Sidebar.vue │ ├── SidebarLink.vue │ ├── Home.vue │ ├── Page.vue │ ├── AlgoliaSearchBox.vue │ ├── NavLinks.vue │ ├── DropdownLink.vue │ ├── Layout.vue │ ├── SearchBox.vue │ └── util.js ├── app │ ├── index.dev.html │ ├── ClientOnly.js │ ├── Content.js │ ├── serverEntry.js │ ├── util.js │ ├── index.ssr.html │ ├── clientEntry.js │ ├── dataMixin.js │ └── app.js ├── markdown │ ├── hoist.js │ ├── slugify.js │ ├── containers.js │ ├── highlight.js │ ├── index.js │ ├── highlightLines.js │ ├── link.js │ └── component.js ├── webpack │ ├── HeadPlugin.js │ ├── createServerConfig.js │ ├── createClientConfig.js │ ├── ClientPlugin.js │ ├── markdownLoader.js │ └── createBaseConfig.js ├── eject.js ├── util │ └── index.js ├── dev.js └── build.js ├── .gitignore ├── docs ├── face.jpg ├── favicon.ico ├── icons │ ├── 192.png │ ├── 512.png │ └── favicon.png ├── assets │ ├── img │ │ ├── brand.734f817b.jpg │ │ └── face.55c9d89d.jpg │ ├── fonts │ │ ├── Roboto-Bold.39b2c303.woff2 │ │ ├── Roboto-Bold.dc81817d.woff │ │ ├── Roboto-Bold.e31fcf18.ttf │ │ ├── Roboto-Light.3b813c2a.woff │ │ ├── Roboto-Light.46e48ce0.ttf │ │ ├── Roboto-Medium.894a2ede.ttf │ │ ├── Roboto-Thin.7500519d.woff │ │ ├── Roboto-Thin.94998475.ttf │ │ ├── Roboto-Thin.954bbdeb.woff2 │ │ ├── fa-brands-400.c601db56.ttf │ │ ├── fa-brands-400.e2a7835b.eot │ │ ├── fa-solid-900.24f9359f.eot │ │ ├── fa-solid-900.4ff89f93.woff │ │ ├── fa-solid-900.af4698a4.ttf │ │ ├── Roboto-Light.69f8a061.woff2 │ │ ├── Roboto-Medium.574fd0b5.woff2 │ │ ├── Roboto-Medium.fc78759e.woff │ │ ├── Roboto-Regular.ba3dcd89.woff │ │ ├── Roboto-Regular.df7b648c.ttf │ │ ├── fa-brands-400.9404b3cb.woff2 │ │ ├── fa-brands-400.cc6aff50.woff │ │ ├── fa-regular-400.0b697cf4.ttf │ │ ├── fa-regular-400.8c986198.woff │ │ ├── fa-regular-400.e07d72d7.eot │ │ ├── fa-solid-900.9c39a8a4.woff2 │ │ ├── Roboto-Regular.2751ee43.woff2 │ │ └── fa-regular-400.28ec6d38.woff2 │ └── js │ │ ├── 0.9e19aec0.js │ │ ├── 7.5635fa08.js │ │ ├── 8.094ad6fb.js │ │ └── 4.7a34dcbf.js ├── manifest.json ├── 404.html └── about │ └── index.html ├── .npmignore ├── README.md ├── .eslintrc.js ├── .editorconfig ├── LICENSE ├── package.json └── bin └── vuepress.js /src/.vuepress/components/.gitkeep: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /lib/noop.js: -------------------------------------------------------------------------------- 1 | module.exports = {} 2 | -------------------------------------------------------------------------------- /src/index.md: -------------------------------------------------------------------------------- 1 | --- 2 | layout: home 3 | --- 4 | -------------------------------------------------------------------------------- /src/.vuepress/override.styl: -------------------------------------------------------------------------------- 1 | // override theme config 2 | 3 | -------------------------------------------------------------------------------- /src/about/index.md: -------------------------------------------------------------------------------- 1 | --- 2 | layout: about 3 | title: 关于我 4 | --- 5 | -------------------------------------------------------------------------------- /src/tags/index.md: -------------------------------------------------------------------------------- 1 | --- 2 | layout: tags 3 | title: Tags 4 | --- 5 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .DS_Store 2 | node_modules 3 | *.log 4 | .temp 5 | vuepress 6 | TODOs.md 7 | -------------------------------------------------------------------------------- /docs/face.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/yscoder/vuepress-theme-indigo/HEAD/docs/face.jpg -------------------------------------------------------------------------------- /docs/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/yscoder/vuepress-theme-indigo/HEAD/docs/favicon.ico -------------------------------------------------------------------------------- /docs/icons/192.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/yscoder/vuepress-theme-indigo/HEAD/docs/icons/192.png -------------------------------------------------------------------------------- /docs/icons/512.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/yscoder/vuepress-theme-indigo/HEAD/docs/icons/512.png -------------------------------------------------------------------------------- /docs/icons/favicon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/yscoder/vuepress-theme-indigo/HEAD/docs/icons/favicon.png -------------------------------------------------------------------------------- /.npmignore: -------------------------------------------------------------------------------- 1 | *.log 2 | .temp 3 | vuepress 4 | TODOs.md 5 | art 6 | test 7 | docs 8 | .eslintrc.js 9 | yarn.lock 10 | -------------------------------------------------------------------------------- /src/.vuepress/public/face.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/yscoder/vuepress-theme-indigo/HEAD/src/.vuepress/public/face.jpg -------------------------------------------------------------------------------- /docs/assets/img/brand.734f817b.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/yscoder/vuepress-theme-indigo/HEAD/docs/assets/img/brand.734f817b.jpg -------------------------------------------------------------------------------- /docs/assets/img/face.55c9d89d.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/yscoder/vuepress-theme-indigo/HEAD/docs/assets/img/face.55c9d89d.jpg -------------------------------------------------------------------------------- /src/.vuepress/public/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/yscoder/vuepress-theme-indigo/HEAD/src/.vuepress/public/favicon.ico -------------------------------------------------------------------------------- /src/.vuepress/public/icons/192.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/yscoder/vuepress-theme-indigo/HEAD/src/.vuepress/public/icons/192.png -------------------------------------------------------------------------------- /src/.vuepress/public/icons/512.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/yscoder/vuepress-theme-indigo/HEAD/src/.vuepress/public/icons/512.png -------------------------------------------------------------------------------- /src/.vuepress/theme/imgs/brand.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/yscoder/vuepress-theme-indigo/HEAD/src/.vuepress/theme/imgs/brand.jpg -------------------------------------------------------------------------------- /src/.vuepress/public/icons/favicon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/yscoder/vuepress-theme-indigo/HEAD/src/.vuepress/public/icons/favicon.png -------------------------------------------------------------------------------- /docs/assets/fonts/Roboto-Bold.39b2c303.woff2: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/yscoder/vuepress-theme-indigo/HEAD/docs/assets/fonts/Roboto-Bold.39b2c303.woff2 -------------------------------------------------------------------------------- /docs/assets/fonts/Roboto-Bold.dc81817d.woff: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/yscoder/vuepress-theme-indigo/HEAD/docs/assets/fonts/Roboto-Bold.dc81817d.woff -------------------------------------------------------------------------------- /docs/assets/fonts/Roboto-Bold.e31fcf18.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/yscoder/vuepress-theme-indigo/HEAD/docs/assets/fonts/Roboto-Bold.e31fcf18.ttf -------------------------------------------------------------------------------- /docs/assets/fonts/Roboto-Light.3b813c2a.woff: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/yscoder/vuepress-theme-indigo/HEAD/docs/assets/fonts/Roboto-Light.3b813c2a.woff -------------------------------------------------------------------------------- /docs/assets/fonts/Roboto-Light.46e48ce0.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/yscoder/vuepress-theme-indigo/HEAD/docs/assets/fonts/Roboto-Light.46e48ce0.ttf -------------------------------------------------------------------------------- /docs/assets/fonts/Roboto-Medium.894a2ede.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/yscoder/vuepress-theme-indigo/HEAD/docs/assets/fonts/Roboto-Medium.894a2ede.ttf -------------------------------------------------------------------------------- /docs/assets/fonts/Roboto-Thin.7500519d.woff: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/yscoder/vuepress-theme-indigo/HEAD/docs/assets/fonts/Roboto-Thin.7500519d.woff -------------------------------------------------------------------------------- /docs/assets/fonts/Roboto-Thin.94998475.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/yscoder/vuepress-theme-indigo/HEAD/docs/assets/fonts/Roboto-Thin.94998475.ttf -------------------------------------------------------------------------------- /docs/assets/fonts/Roboto-Thin.954bbdeb.woff2: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/yscoder/vuepress-theme-indigo/HEAD/docs/assets/fonts/Roboto-Thin.954bbdeb.woff2 -------------------------------------------------------------------------------- /docs/assets/fonts/fa-brands-400.c601db56.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/yscoder/vuepress-theme-indigo/HEAD/docs/assets/fonts/fa-brands-400.c601db56.ttf -------------------------------------------------------------------------------- /docs/assets/fonts/fa-brands-400.e2a7835b.eot: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/yscoder/vuepress-theme-indigo/HEAD/docs/assets/fonts/fa-brands-400.e2a7835b.eot -------------------------------------------------------------------------------- /docs/assets/fonts/fa-solid-900.24f9359f.eot: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/yscoder/vuepress-theme-indigo/HEAD/docs/assets/fonts/fa-solid-900.24f9359f.eot -------------------------------------------------------------------------------- /docs/assets/fonts/fa-solid-900.4ff89f93.woff: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/yscoder/vuepress-theme-indigo/HEAD/docs/assets/fonts/fa-solid-900.4ff89f93.woff -------------------------------------------------------------------------------- /docs/assets/fonts/fa-solid-900.af4698a4.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/yscoder/vuepress-theme-indigo/HEAD/docs/assets/fonts/fa-solid-900.af4698a4.ttf -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # vuepress-theme-indigo 2 | 3 | > 这个仓库是 [hexo-theme-indigo](https://github.com/yscoder/hexo-theme-indigo) 主题的 vuepress 版本。 4 | 5 | 开发中,敬请期待! 6 | -------------------------------------------------------------------------------- /docs/assets/fonts/Roboto-Light.69f8a061.woff2: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/yscoder/vuepress-theme-indigo/HEAD/docs/assets/fonts/Roboto-Light.69f8a061.woff2 -------------------------------------------------------------------------------- /docs/assets/fonts/Roboto-Medium.574fd0b5.woff2: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/yscoder/vuepress-theme-indigo/HEAD/docs/assets/fonts/Roboto-Medium.574fd0b5.woff2 -------------------------------------------------------------------------------- /docs/assets/fonts/Roboto-Medium.fc78759e.woff: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/yscoder/vuepress-theme-indigo/HEAD/docs/assets/fonts/Roboto-Medium.fc78759e.woff -------------------------------------------------------------------------------- /docs/assets/fonts/Roboto-Regular.ba3dcd89.woff: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/yscoder/vuepress-theme-indigo/HEAD/docs/assets/fonts/Roboto-Regular.ba3dcd89.woff -------------------------------------------------------------------------------- /docs/assets/fonts/Roboto-Regular.df7b648c.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/yscoder/vuepress-theme-indigo/HEAD/docs/assets/fonts/Roboto-Regular.df7b648c.ttf -------------------------------------------------------------------------------- /docs/assets/fonts/fa-brands-400.9404b3cb.woff2: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/yscoder/vuepress-theme-indigo/HEAD/docs/assets/fonts/fa-brands-400.9404b3cb.woff2 -------------------------------------------------------------------------------- /docs/assets/fonts/fa-brands-400.cc6aff50.woff: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/yscoder/vuepress-theme-indigo/HEAD/docs/assets/fonts/fa-brands-400.cc6aff50.woff -------------------------------------------------------------------------------- /docs/assets/fonts/fa-regular-400.0b697cf4.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/yscoder/vuepress-theme-indigo/HEAD/docs/assets/fonts/fa-regular-400.0b697cf4.ttf -------------------------------------------------------------------------------- /docs/assets/fonts/fa-regular-400.8c986198.woff: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/yscoder/vuepress-theme-indigo/HEAD/docs/assets/fonts/fa-regular-400.8c986198.woff -------------------------------------------------------------------------------- /docs/assets/fonts/fa-regular-400.e07d72d7.eot: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/yscoder/vuepress-theme-indigo/HEAD/docs/assets/fonts/fa-regular-400.e07d72d7.eot -------------------------------------------------------------------------------- /docs/assets/fonts/fa-solid-900.9c39a8a4.woff2: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/yscoder/vuepress-theme-indigo/HEAD/docs/assets/fonts/fa-solid-900.9c39a8a4.woff2 -------------------------------------------------------------------------------- /docs/assets/fonts/Roboto-Regular.2751ee43.woff2: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/yscoder/vuepress-theme-indigo/HEAD/docs/assets/fonts/Roboto-Regular.2751ee43.woff2 -------------------------------------------------------------------------------- /docs/assets/fonts/fa-regular-400.28ec6d38.woff2: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/yscoder/vuepress-theme-indigo/HEAD/docs/assets/fonts/fa-regular-400.28ec6d38.woff2 -------------------------------------------------------------------------------- /lib/index.js: -------------------------------------------------------------------------------- 1 | exports.dev = require('./dev') 2 | exports.build = require('./build') 3 | exports.eject = require('./eject') 4 | Object.assign(exports, require('./util')) 5 | -------------------------------------------------------------------------------- /src/.vuepress/theme/styles/fonts/roboto/Roboto-Bold.eot: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/yscoder/vuepress-theme-indigo/HEAD/src/.vuepress/theme/styles/fonts/roboto/Roboto-Bold.eot -------------------------------------------------------------------------------- /src/.vuepress/theme/styles/fonts/roboto/Roboto-Bold.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/yscoder/vuepress-theme-indigo/HEAD/src/.vuepress/theme/styles/fonts/roboto/Roboto-Bold.ttf -------------------------------------------------------------------------------- /src/.vuepress/theme/styles/fonts/roboto/Roboto-Bold.woff: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/yscoder/vuepress-theme-indigo/HEAD/src/.vuepress/theme/styles/fonts/roboto/Roboto-Bold.woff -------------------------------------------------------------------------------- /src/.vuepress/theme/styles/fonts/roboto/Roboto-Light.eot: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/yscoder/vuepress-theme-indigo/HEAD/src/.vuepress/theme/styles/fonts/roboto/Roboto-Light.eot -------------------------------------------------------------------------------- /src/.vuepress/theme/styles/fonts/roboto/Roboto-Light.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/yscoder/vuepress-theme-indigo/HEAD/src/.vuepress/theme/styles/fonts/roboto/Roboto-Light.ttf -------------------------------------------------------------------------------- /src/.vuepress/theme/styles/fonts/roboto/Roboto-Thin.eot: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/yscoder/vuepress-theme-indigo/HEAD/src/.vuepress/theme/styles/fonts/roboto/Roboto-Thin.eot -------------------------------------------------------------------------------- /src/.vuepress/theme/styles/fonts/roboto/Roboto-Thin.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/yscoder/vuepress-theme-indigo/HEAD/src/.vuepress/theme/styles/fonts/roboto/Roboto-Thin.ttf -------------------------------------------------------------------------------- /src/.vuepress/theme/styles/fonts/roboto/Roboto-Thin.woff: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/yscoder/vuepress-theme-indigo/HEAD/src/.vuepress/theme/styles/fonts/roboto/Roboto-Thin.woff -------------------------------------------------------------------------------- /src/.vuepress/theme/styles/fonts/roboto/Roboto-Bold.woff2: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/yscoder/vuepress-theme-indigo/HEAD/src/.vuepress/theme/styles/fonts/roboto/Roboto-Bold.woff2 -------------------------------------------------------------------------------- /src/.vuepress/theme/styles/fonts/roboto/Roboto-Light.woff: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/yscoder/vuepress-theme-indigo/HEAD/src/.vuepress/theme/styles/fonts/roboto/Roboto-Light.woff -------------------------------------------------------------------------------- /src/.vuepress/theme/styles/fonts/roboto/Roboto-Light.woff2: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/yscoder/vuepress-theme-indigo/HEAD/src/.vuepress/theme/styles/fonts/roboto/Roboto-Light.woff2 -------------------------------------------------------------------------------- /src/.vuepress/theme/styles/fonts/roboto/Roboto-Medium.eot: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/yscoder/vuepress-theme-indigo/HEAD/src/.vuepress/theme/styles/fonts/roboto/Roboto-Medium.eot -------------------------------------------------------------------------------- /src/.vuepress/theme/styles/fonts/roboto/Roboto-Medium.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/yscoder/vuepress-theme-indigo/HEAD/src/.vuepress/theme/styles/fonts/roboto/Roboto-Medium.ttf -------------------------------------------------------------------------------- /src/.vuepress/theme/styles/fonts/roboto/Roboto-Medium.woff: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/yscoder/vuepress-theme-indigo/HEAD/src/.vuepress/theme/styles/fonts/roboto/Roboto-Medium.woff -------------------------------------------------------------------------------- /src/.vuepress/theme/styles/fonts/roboto/Roboto-Medium.woff2: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/yscoder/vuepress-theme-indigo/HEAD/src/.vuepress/theme/styles/fonts/roboto/Roboto-Medium.woff2 -------------------------------------------------------------------------------- /src/.vuepress/theme/styles/fonts/roboto/Roboto-Regular.eot: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/yscoder/vuepress-theme-indigo/HEAD/src/.vuepress/theme/styles/fonts/roboto/Roboto-Regular.eot -------------------------------------------------------------------------------- /src/.vuepress/theme/styles/fonts/roboto/Roboto-Regular.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/yscoder/vuepress-theme-indigo/HEAD/src/.vuepress/theme/styles/fonts/roboto/Roboto-Regular.ttf -------------------------------------------------------------------------------- /src/.vuepress/theme/styles/fonts/roboto/Roboto-Regular.woff: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/yscoder/vuepress-theme-indigo/HEAD/src/.vuepress/theme/styles/fonts/roboto/Roboto-Regular.woff -------------------------------------------------------------------------------- /src/.vuepress/theme/styles/fonts/roboto/Roboto-Thin.woff2: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/yscoder/vuepress-theme-indigo/HEAD/src/.vuepress/theme/styles/fonts/roboto/Roboto-Thin.woff2 -------------------------------------------------------------------------------- /.eslintrc.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | root: true, 3 | extends: ['plugin:vue-libs/recommended'], 4 | rules: { 5 | indent: ['error', 2, { MemberExpression: 'off' }] 6 | } 7 | } 8 | -------------------------------------------------------------------------------- /src/.vuepress/theme/styles/fonts/roboto/Roboto-Regular.woff2: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/yscoder/vuepress-theme-indigo/HEAD/src/.vuepress/theme/styles/fonts/roboto/Roboto-Regular.woff2 -------------------------------------------------------------------------------- /.editorconfig: -------------------------------------------------------------------------------- 1 | root = true 2 | 3 | [*] 4 | charset = utf-8 5 | indent_style = space 6 | indent_size = 2 7 | insert_final_newline = true 8 | trim_trailing_whitespace = true 9 | 10 | [*.{js}] 11 | indent_size = 4 12 | -------------------------------------------------------------------------------- /src/.vuepress/theme/About.vue: -------------------------------------------------------------------------------- 1 | 6 | 11 | 13 | -------------------------------------------------------------------------------- /src/.vuepress/theme/NotFound.vue: -------------------------------------------------------------------------------- 1 | 6 | 11 | 13 | -------------------------------------------------------------------------------- /src/.vuepress/theme/styles/theme.styl: -------------------------------------------------------------------------------- 1 | @import './config.styl' 2 | @import '~vuetify/src/stylus/app.styl' 3 | @import '~vuetify/src/stylus/main.styl' 4 | @import './roboto.styl' 5 | @import './global.styl' 6 | @import './content.styl' 7 | -------------------------------------------------------------------------------- /lib/default-theme/search.svg: -------------------------------------------------------------------------------- 1 | 2 | -------------------------------------------------------------------------------- /src/.vuepress/ignoreStylus.js: -------------------------------------------------------------------------------- 1 | const loaderUtils = require('loader-utils') 2 | const template = '' 3 | 4 | module.exports = function (source) { 5 | // console.log(source) 6 | return source.replace(/require.+src\/stylus\/components\/.+.styl'\);/g, '') 7 | } 8 | -------------------------------------------------------------------------------- /docs/assets/js/0.9e19aec0.js: -------------------------------------------------------------------------------- 1 | (window.webpackJsonp=window.webpackJsonp||[]).push([[0],{353:function(t,n,e){"use strict";e.r(n);var s=e(0),c=Object(s.a)({},function(){var t=this.$createElement;return(this._self._c||t)("div",{staticClass:"content"})},[],!1,null,null,null);n.default=c.exports}}]); -------------------------------------------------------------------------------- /docs/assets/js/7.5635fa08.js: -------------------------------------------------------------------------------- 1 | (window.webpackJsonp=window.webpackJsonp||[]).push([[7],{355:function(t,n,e){"use strict";e.r(n);var s=e(0),c=Object(s.a)({},function(){var t=this.$createElement;return(this._self._c||t)("div",{staticClass:"content"})},[],!1,null,null,null);n.default=c.exports}}]); -------------------------------------------------------------------------------- /docs/assets/js/8.094ad6fb.js: -------------------------------------------------------------------------------- 1 | (window.webpackJsonp=window.webpackJsonp||[]).push([[8],{354:function(t,n,e){"use strict";e.r(n);var s=e(0),c=Object(s.a)({},function(){var t=this.$createElement;return(this._self._c||t)("div",{staticClass:"content"})},[],!1,null,null,null);n.default=c.exports}}]); -------------------------------------------------------------------------------- /src/.vuepress/theme/libs/i18n.js: -------------------------------------------------------------------------------- 1 | export default { 2 | install(Vue, lang) { 3 | const locals = require(`../languages/${lang}`) 4 | 5 | // $tt('hello') -> '你好' 6 | Vue.prototype.$tt = function(field) { 7 | return locals[field] || field 8 | } 9 | } 10 | } 11 | -------------------------------------------------------------------------------- /lib/app/index.dev.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 |
10 | 11 | 12 | -------------------------------------------------------------------------------- /lib/app/ClientOnly.js: -------------------------------------------------------------------------------- 1 | export default { 2 | functional: true, 3 | render (h, { parent, children }) { 4 | if (parent._isMounted) { 5 | return children 6 | } else { 7 | parent.$once('hook:mounted', () => { 8 | parent.$forceUpdate() 9 | }) 10 | } 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /src/.vuepress/theme/components/Comment.vue: -------------------------------------------------------------------------------- 1 | 8 | 13 | 15 | -------------------------------------------------------------------------------- /src/.vuepress/theme/languages/zh-CN.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | license: `博客内容遵循 知识共享 署名 - 非商业性 - 相同方式共享 4.0 国际协议`, 3 | postNav_prev: 'Prev', 4 | postNav_next: 'Next', 5 | copyLink: '复制链接' 6 | } 7 | -------------------------------------------------------------------------------- /src/.vuepress/theme/languages/en-US.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | license: `This blog is licensed under a Creative Commons Attribution 4.0 International License`, 3 | postNav_prev: 'Prev', 4 | postNav_Next: 'Next', 5 | copyLink: 'CopyLink' 6 | } 7 | -------------------------------------------------------------------------------- /lib/app/Content.js: -------------------------------------------------------------------------------- 1 | import { pathToComponentName } from './util' 2 | 3 | export default { 4 | functional: true, 5 | 6 | props: { 7 | custom: { 8 | type: Boolean, 9 | default: true 10 | } 11 | }, 12 | 13 | render (h, { parent, props }) { 14 | return h(pathToComponentName(parent.$page.path), { 15 | class: props.custom ? 'custom' : '' 16 | }) 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /lib/app/serverEntry.js: -------------------------------------------------------------------------------- 1 | import { createApp } from './app' 2 | 3 | export default context => new Promise((resolve, reject) => { 4 | const { app, router } = createApp() 5 | const { url } = context 6 | const { fullPath } = router.resolve(url).route 7 | 8 | if (fullPath !== url) { 9 | return reject({ url: fullPath }) 10 | } 11 | 12 | router.push(url) 13 | router.onReady(() => resolve(app)) 14 | }) 15 | -------------------------------------------------------------------------------- /lib/markdown/hoist.js: -------------------------------------------------------------------------------- 1 | module.exports = md => { 2 | const RE = /^<(script|style)(?=(\s|>|$))/i 3 | 4 | md.renderer.rules.html_block = (tokens, idx) => { 5 | const content = tokens[idx].content 6 | const hoistedTags = md.__data.hoistedTags || (md.__data.hoistedTags = []) 7 | if (RE.test(content.trim())) { 8 | hoistedTags.push(content) 9 | return '' 10 | } else { 11 | return content 12 | } 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /lib/default-theme/styles/config.styl: -------------------------------------------------------------------------------- 1 | // colors 2 | $accentColor = #3eaf7c 3 | $textColor = #2c3e50 4 | $borderColor = #eaecef 5 | $codeBgColor = #282c34 6 | $arrowBgColor = #ccc 7 | 8 | // layout 9 | $navbarHeight = 3.6rem 10 | $sidebarWidth = 20rem 11 | $contentWidth = 740px 12 | 13 | // responsive breakpoints 14 | $MQNarrow = 959px 15 | $MQMobile = 719px 16 | $MQMobileNarrow = 419px 17 | 18 | @import '~@temp/override.styl' // generated from user config 19 | -------------------------------------------------------------------------------- /docs/manifest.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "YusenBlog", 3 | "short_name": "YusenBlog", 4 | "icons": [ 5 | { 6 | "src": "./icons/192.png", 7 | "sizes": "192x192", 8 | "type": "image/png" 9 | }, 10 | { 11 | "src": "./icons/512.png", 12 | "sizes": "512x512", 13 | "type": "image/png" 14 | } 15 | ], 16 | "start_url": "./index.html", 17 | "display": "standalone", 18 | "background_color": "#fff", 19 | "theme_color": "#3F51B5" 20 | } 21 | -------------------------------------------------------------------------------- /src/.vuepress/theme/enhanceApp.js: -------------------------------------------------------------------------------- 1 | import i18n from './libs/i18n' 2 | import blog from './libs/blog' 3 | import routes from './libs/routes' 4 | import components from './components' 5 | import './styles/theme.styl' 6 | 7 | export default ({ Vue, options, router, siteData }) => { 8 | const { themeConfig: theme, pages } = siteData 9 | Vue.use(i18n, theme.lang) 10 | Vue.use(blog, { theme, pages }) 11 | Vue.use(routes, { router, theme }) 12 | Vue.use(components, theme) 13 | } 14 | -------------------------------------------------------------------------------- /lib/app/util.js: -------------------------------------------------------------------------------- 1 | export function pathToComponentName (path) { 2 | if (path.charAt(path.length - 1) === '/') { 3 | return `page${path.replace(/\//g, '-') + 'index'}` 4 | } else { 5 | return `page${path.replace(/\//g, '-').replace(/\.html$/, '')}` 6 | } 7 | } 8 | 9 | export function findPageForPath (pages, path) { 10 | for (let i = 0; i < pages.length; i++) { 11 | const page = pages[i] 12 | if (page.path === path) { 13 | return page 14 | } 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /src/.vuepress/public/manifest.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "YusenBlog", 3 | "short_name": "YusenBlog", 4 | "icons": [ 5 | { 6 | "src": "./icons/192.png", 7 | "sizes": "192x192", 8 | "type": "image/png" 9 | }, 10 | { 11 | "src": "./icons/512.png", 12 | "sizes": "512x512", 13 | "type": "image/png" 14 | } 15 | ], 16 | "start_url": "./index.html", 17 | "display": "standalone", 18 | "background_color": "#fff", 19 | "theme_color": "#3F51B5" 20 | } 21 | -------------------------------------------------------------------------------- /lib/app/index.ssr.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | {{ title }} 7 | 8 | {{{ userHeadTags }}} 9 | {{{ pageMeta }}} 10 | {{{ renderResourceHints() }}} 11 | {{{ renderStyles() }}} 12 | 13 | 14 | 15 | {{{ renderScripts() }}} 16 | 17 | 18 | -------------------------------------------------------------------------------- /lib/default-theme/NotFound.vue: -------------------------------------------------------------------------------- 1 | 9 | 10 | 26 | -------------------------------------------------------------------------------- /src/.vuepress/theme/components/PostTime.vue: -------------------------------------------------------------------------------- 1 | 9 | 20 | 25 | 26 | -------------------------------------------------------------------------------- /lib/default-theme/OutboundLink.vue: -------------------------------------------------------------------------------- 1 | 7 | 8 | 13 | -------------------------------------------------------------------------------- /lib/webpack/HeadPlugin.js: -------------------------------------------------------------------------------- 1 | const { normalizeHeadTag } = require('../util') 2 | 3 | module.exports = class SiteDataPlugin { 4 | constructor ({ tags }) { 5 | this.tags = tags 6 | } 7 | 8 | apply (compiler) { 9 | compiler.hooks.compilation.tap('vuepress-site-data', compilation => { 10 | compilation.hooks.htmlWebpackPluginAlterAssetTags.tapAsync('vuepress-site-data', (data, cb) => { 11 | try { 12 | this.tags.forEach(tag => { 13 | data.head.push(normalizeHeadTag(tag)) 14 | }) 15 | } catch (e) { 16 | return cb(e) 17 | } 18 | cb(null, data) 19 | }) 20 | }) 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /lib/eject.js: -------------------------------------------------------------------------------- 1 | const fs = require('fs-extra') 2 | const path = require('path') 3 | const chalk = require('chalk') 4 | 5 | module.exports = async (dir) => { 6 | const source = path.resolve(__dirname, 'default-theme') 7 | const target = path.resolve(dir, '.vuepress/theme') 8 | await fs.copy(source, target) 9 | // remove the import to default theme override 10 | const styleConfig = path.resolve(target, 'styles/config.styl') 11 | const content = await fs.readFile(styleConfig, 'utf-8') 12 | const transformed = content.split('\n').slice(0, -2).join('\n') 13 | await fs.writeFile(styleConfig, transformed) 14 | console.log(`Copied default theme into ${chalk.cyan(target)}.`) 15 | } 16 | -------------------------------------------------------------------------------- /lib/default-theme/styles/arrow.styl: -------------------------------------------------------------------------------- 1 | @require './config' 2 | 3 | .arrow 4 | display inline-block 5 | width 0 6 | height 0 7 | &.up 8 | border-left 4px solid transparent 9 | border-right 4px solid transparent 10 | border-bottom 6px solid $arrowBgColor 11 | &.down 12 | border-left 4px solid transparent 13 | border-right 4px solid transparent 14 | border-top 6px solid $arrowBgColor 15 | &.right 16 | border-top 4px solid transparent 17 | border-bottom 4px solid transparent 18 | border-left 6px solid $arrowBgColor 19 | &.left 20 | border-top 4px solid transparent 21 | border-bottom 4px solid transparent 22 | border-right 6px solid $arrowBgColor 23 | -------------------------------------------------------------------------------- /lib/default-theme/DropdownTransition.vue: -------------------------------------------------------------------------------- 1 | 9 | 10 | 24 | 25 | 30 | -------------------------------------------------------------------------------- /src/.vuepress/theme/libs/utils.js: -------------------------------------------------------------------------------- 1 | export function pathToComponentName(path) { 2 | if (path.charAt(path.length - 1) === '/') { 3 | return `page${path.replace(/\//g, '-') + 'index'}` 4 | } else { 5 | return `page${path.replace(/\//g, '-').replace(/\.html$/, '')}` 6 | } 7 | } 8 | 9 | export function updateMetaTags(meta, current) { 10 | if (current) { 11 | current.forEach(c => { 12 | document.head.removeChild(c) 13 | }) 14 | } 15 | if (meta) { 16 | return meta.map(m => { 17 | const tag = document.createElement('meta') 18 | Object.keys(m).forEach(key => { 19 | tag.setAttribute(key, m[key]) 20 | }) 21 | document.head.appendChild(tag) 22 | return tag 23 | }) 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /lib/default-theme/NavLink.vue: -------------------------------------------------------------------------------- 1 | 16 | 17 | 37 | -------------------------------------------------------------------------------- /lib/default-theme/styles/custom-blocks.styl: -------------------------------------------------------------------------------- 1 | .custom-block 2 | .custom-block-title 3 | font-weight 600 4 | margin-bottom -0.4rem 5 | &.tip, &.warning, &.danger 6 | padding .1rem 1.5rem 7 | border-left-width .5rem 8 | border-left-style solid 9 | margin 1rem 0 10 | &.tip 11 | background-color #f3f5f7 12 | border-color #42b983 13 | &.warning 14 | background-color rgba(255,229,100,.3) 15 | border-color darken(#ffe564, 35%) 16 | color darken(#ffe564, 70%) 17 | .custom-block-title 18 | color darken(#ffe564, 50%) 19 | a 20 | color $textColor 21 | &.danger 22 | background-color #ffe6e6 23 | border-color darken(red, 20%) 24 | color darken(red, 70%) 25 | .custom-block-title 26 | color darken(red, 40%) 27 | a 28 | color $textColor 29 | -------------------------------------------------------------------------------- /src/.vuepress/theme/libs/routes.js: -------------------------------------------------------------------------------- 1 | const Layout = () => import('../Layout') 2 | 3 | const install = (Vue, { router, theme }) => { 4 | const { pagination, tags, categories } = theme 5 | const routes = [] 6 | 7 | if (pagination) 8 | routes.push({ 9 | path: pagination.path, 10 | component: Layout, 11 | meta: { 12 | layout: 'home' 13 | } 14 | }) 15 | 16 | if (tags) 17 | routes.push({ 18 | path: tags.path, 19 | component: Layout, 20 | meta: { 21 | layout: 'tags' 22 | } 23 | }) 24 | 25 | if (categories) 26 | routes.push({ 27 | path: categories.path, 28 | component: Layout, 29 | meta: { 30 | layout: 'categories' 31 | } 32 | }) 33 | 34 | router.addRoutes(routes) 35 | } 36 | 37 | export default { install } 38 | -------------------------------------------------------------------------------- /lib/markdown/slugify.js: -------------------------------------------------------------------------------- 1 | // string.js slugify drops non ascii chars so we have to 2 | // use a custom implementation here 3 | const removeDiacritics = require('diacritics').remove 4 | // eslint-disable-next-line no-control-regex 5 | const rControl = /[\u0000-\u001f]/g 6 | const rSpecial = /[\s~`!@#$%^&*()\-_+=[\]{}|\\;:"'<>,.?/]+/g 7 | 8 | module.exports = function slugify (str) { 9 | return removeDiacritics(str) 10 | // Remove control characters 11 | .replace(rControl, '') 12 | // Replace special characters 13 | .replace(rSpecial, '-') 14 | // Remove continous separators 15 | .replace(/\-{2,}/g, '-') 16 | // Remove prefixing and trailing separtors 17 | .replace(/^\-+|\-+$/g, '') 18 | // ensure it doesn't start with a number (#121) 19 | .replace(/^(\d)/, '_$1') 20 | // lowercase 21 | .toLowerCase() 22 | } 23 | -------------------------------------------------------------------------------- /lib/default-theme/styles/mobile.styl: -------------------------------------------------------------------------------- 1 | @require './config' 2 | 3 | $mobileSidebarWidth = $sidebarWidth * 0.82 4 | 5 | // narrow desktop / iPad 6 | @media (max-width: $MQNarrow) 7 | .sidebar 8 | font-size 15px 9 | width $mobileSidebarWidth 10 | .page 11 | padding-left $mobileSidebarWidth 12 | .content:not(.custom) 13 | padding 2rem 14 | 15 | // wide mobile 16 | @media (max-width: $MQMobile) 17 | .sidebar 18 | top 0 19 | padding-top $navbarHeight 20 | transform translateX(-100%) 21 | transition transform .2s ease 22 | .page 23 | padding-left 0 24 | .theme-container.sidebar-open 25 | .sidebar 26 | transform translateX(0) 27 | 28 | // narrow mobile 29 | @media (max-width: $MQMobileNarrow) 30 | h1 31 | font-size 1.9rem 32 | .content:not(.custom) 33 | padding 1.5rem 34 | .content 35 | pre, pre[class*="language-"] 36 | margin 0.85rem -1.5rem 37 | border-radius 0 38 | -------------------------------------------------------------------------------- /lib/markdown/containers.js: -------------------------------------------------------------------------------- 1 | const container = require('markdown-it-container') 2 | 3 | module.exports = md => { 4 | md 5 | .use(...createContainer('tip', 'TIP')) 6 | .use(...createContainer('warning', 'WARNING')) 7 | .use(...createContainer('danger', 'WARNING')) 8 | // explicitly escape Vue syntax 9 | .use(container, 'v-pre', { 10 | render: (tokens, idx) => tokens[idx].nesting === 1 11 | ? `
\n` 12 | : `
\n` 13 | }) 14 | } 15 | 16 | function createContainer (klass, defaultTitle) { 17 | return [container, klass, { 18 | render (tokens, idx) { 19 | const token = tokens[idx] 20 | const info = token.info.trim().slice(klass.length).trim() 21 | if (token.nesting === 1) { 22 | return `

${info || defaultTitle}

\n` 23 | } else { 24 | return `
\n` 25 | } 26 | } 27 | }] 28 | } 29 | -------------------------------------------------------------------------------- /src/.vuepress/theme/styles/global.styl: -------------------------------------------------------------------------------- 1 | html 2 | overflow-y: auto 3 | 4 | img 5 | max-width 100% 6 | 7 | ol.reset, 8 | ul.reset 9 | list-style none 10 | li 11 | list-style none 12 | 13 | a 14 | text-decoration: inherit 15 | 16 | .application--wrap 17 | background $body-background 18 | 19 | .blog-container 20 | max-width $container-max-width!important 21 | min-height: calc(100vh - 56px - 122px) 22 | 23 | .title, 24 | .headline, 25 | .subheading, 26 | .capitalize 27 | text-transform: capitalize 28 | 29 | .fake-hide 30 | position: absolute; 31 | top: -9999px; 32 | right: -9999px; 33 | z-index: -1; 34 | height: 1px; 35 | width: 1px; 36 | margin: -1px; 37 | padding: 0; 38 | border: none; 39 | overflow: hidden; 40 | clip: rect(0, 0, 0, 0); 41 | opacity: 0; 42 | 43 | .blog-progress.progress-linear 44 | position fixed 45 | top 0 46 | left 0 47 | right 0 48 | z-index 9999 49 | margin 0 50 | background: #fff 51 | -------------------------------------------------------------------------------- /src/.vuepress/theme/libs/socials.js: -------------------------------------------------------------------------------- 1 | export default { 2 | Weibo: { 3 | icon: 'weibo', 4 | share(page) { 5 | return `https://service.weibo.com/share/share.php?url=${page.url}&title=${page.title}&pic=${page.pic}` 6 | } 7 | }, 8 | QQ: { 9 | icon: 'qq', 10 | share(page) { 11 | return `https://connect.qq.com/widget/shareqq/index.html?url=${page.url}&title=${page.title}&summary=${page.summary}&pics=${page.pic}` 12 | } 13 | }, 14 | Facebook: { 15 | icon: 'facebook', 16 | share(page) { 17 | return `https://www.facebook.com/sharer/sharer.php?u=${page.url}` 18 | } 19 | }, 20 | Twitter: { 21 | icon: 'twitter', 22 | share(page) { 23 | return `https://twitter.com/intent/tweet?text=${page.title}&url=${page.url}&via=${page.origin}` 24 | } 25 | }, 26 | GooglePlus: { 27 | icon: 'google-plus-g', 28 | share(page) { 29 | return `https://plus.google.com/share?url=${page.url}` 30 | } 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /src/.vuepress/theme/Home.vue: -------------------------------------------------------------------------------- 1 | 14 | 36 | -------------------------------------------------------------------------------- /src/.vuepress/theme/components/Tag.vue: -------------------------------------------------------------------------------- 1 | 14 | 28 | 46 | 47 | 48 | -------------------------------------------------------------------------------- /lib/default-theme/SidebarButton.vue: -------------------------------------------------------------------------------- 1 | 8 | 9 | 29 | -------------------------------------------------------------------------------- /src/posts/text-truncation.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: 前端文字的截断处理 3 | date: 2015-09-06 17:37:32 4 | tags: [Html, CSS, Ellipsis] 5 | description: 前端文字的截断处理 6 | --- 7 | 关于前端页面的文字溢出截断的招数已经很常见了。 8 | 通常的实现有,前端css控制、后端字数输出控制或者前端js字数处理等。 9 | 10 | 11 | 12 | ## 单行文字 13 | 14 | 单行文字的溢出处理很简单,我通常是使用css来控制,在文字末尾加上`...`。 15 | 16 | ```css 17 | .ellipsis { 18 | overflow: hidden; 19 | text-overflow: ellipsis; 20 | white-space: nowrap; 21 | } 22 | ``` 23 | 24 | 给需要文字截断的节点增加一个这样的基础类,然后设置该节点的宽。 25 | 同时注意给未来可能会出现溢出的节点也加上此类,再设置最大宽度`max-width`,因为很多奇葩用户的输入是你无法掌控的。 - -! 26 | 27 | ## 多行文字 28 | 29 | 我希望在一个的固定高度的容器中,内容超出后,最后一个文字显示`...`。 30 | 31 | 如法炮制给多行文字的容器添加`ellipsis`类后,你会发现的确是显示`...`,不过此时文字是一行的。 32 | 因为在该类中添加了属性`white-space`,用来定义一个段落如何换行。属性值`nowrap`:禁止文本换行,除非遇到`
`。 33 | 34 | 一番思考后,确定了一个方案。 35 | 36 | 1. 给固定高度的容器添加`overflow: hidden`; 37 | 2. 给容器添加相对定位; 38 | 3. 添加伪元素样式,`content:'...'`,绝对定位,然后位置定位在容器末尾。 39 | 40 | 伪元素的兼容性为IE8,如果需要兼容IE7的可以使用标签代替。 41 | 42 | ## 带显示全部的多行文字 43 | 44 | 类似 QQ 空间、微信、微博那种。 45 | 46 | {% jsfiddle imys/wymxhaek/3 html,css,result %} 47 | 48 | 使用 2 个伪元素加 1 个 a 链接实现。 49 | 精妙之处在于使用伪元素遮挡一行文字,让 a 链接位于伪元素之上显示。 50 | -------------------------------------------------------------------------------- /src/.vuepress/theme/Header.vue: -------------------------------------------------------------------------------- 1 | 25 | 42 | 51 | -------------------------------------------------------------------------------- /lib/markdown/highlight.js: -------------------------------------------------------------------------------- 1 | const chalk = require('chalk') 2 | const prism = require('prismjs') 3 | const loadLanguages = require('prismjs/components/index') 4 | const escapeHtml = require('escape-html') 5 | 6 | // required to make embedded highlighting work... 7 | loadLanguages(['markup', 'css', 'javascript']) 8 | 9 | function wrap (code, lang) { 10 | if (lang === 'text') { 11 | code = escapeHtml(code) 12 | } 13 | return `
${code}
` 14 | } 15 | 16 | module.exports = (str, lang) => { 17 | if (!lang) { 18 | return wrap(str, 'text') 19 | } 20 | const rawLang = lang 21 | if (lang === 'vue' || lang === 'html') { 22 | lang = 'markup' 23 | } 24 | if (lang === 'md') { 25 | lang = 'markdown' 26 | } 27 | if (!prism.languages[lang]) { 28 | try { 29 | loadLanguages([lang]) 30 | } catch (e) { 31 | console.log(chalk.yellow(`[vuepress] Syntax highlight for language "${lang}" is not supported.`)) 32 | } 33 | } 34 | if (prism.languages[lang]) { 35 | const code = prism.highlight(str, prism.languages[lang], lang) 36 | return wrap(code, rawLang) 37 | } 38 | return wrap(str, 'text') 39 | } 40 | -------------------------------------------------------------------------------- /lib/default-theme/styles/nprogress.styl: -------------------------------------------------------------------------------- 1 | #nprogress 2 | pointer-events none 3 | .bar 4 | background $accentColor 5 | position fixed 6 | z-index 1031 7 | top 0 8 | left 0 9 | width 100% 10 | height 2px 11 | .peg 12 | display block 13 | position absolute 14 | right 0px 15 | width 100px 16 | height 100% 17 | box-shadow 0 0 10px $accentColor, 0 0 5px $accentColor 18 | opacity 1.0 19 | transform rotate(3deg) translate(0px, -4px) 20 | .spinner 21 | display block 22 | position fixed 23 | z-index 1031 24 | top 15px 25 | right 15px 26 | .spinner-icon 27 | width 18px 28 | height 18px 29 | box-sizing border-box 30 | border solid 2px transparent 31 | border-top-color $accentColor 32 | border-left-color $accentColor 33 | border-radius 50% 34 | animation nprogress-spinner 400ms linear infinite 35 | 36 | .nprogress-custom-parent 37 | overflow hidden 38 | position relative 39 | 40 | .nprogress-custom-parent #nprogress .spinner, 41 | .nprogress-custom-parent #nprogress .bar 42 | position absolute 43 | 44 | @keyframes nprogress-spinner 45 | 0% transform rotate(0deg) 46 | 100% transform rotate(360deg) 47 | -------------------------------------------------------------------------------- /src/.vuepress/theme/styles/config.styl: -------------------------------------------------------------------------------- 1 | $body-font-family = -apple-system,BlinkMacSystemFont,Roboto,"Segoe UI","Helvetica Neue",Helvetica,"PingFang SC", "Hiragino Sans GB","Microsoft YaHei",SimSun,sans-serif 2 | $font-size-root = 14px 3 | $line-height-root = 1.5 4 | $card-border-radius = 4px 5 | 6 | $font-roboto = './fonts/roboto' 7 | $font-code = Consolas, Monaco, 'Andale Mono', 'Ubuntu Mono', monospace 8 | 9 | $container-max-width = 1000px 10 | $body-background = #f6f6f6 11 | $primary-color = #3F51B5 12 | $light-primary-color = lighten($primary-color, 50%) 13 | $content-color = #4d4d4d 14 | $gray-color = #7e7e7e 15 | 16 | $custom-blocks = { 17 | tip: #4caf50 18 | warning: #FF9800 19 | danger: #ff5722 20 | } 21 | 22 | $code-languages = { 23 | js: 'js' 'javascript' 24 | html: 'html' 'markup' 25 | md: 'md' 'markdown' 26 | vue: 'vue' 27 | css: 'css' 28 | less: 'less' 29 | sass: 'sass' 30 | scss: 'scss' 31 | stylus: 'stylus' 32 | jsx: 'jsx' 33 | ts: 'ts' 'typescript' 34 | json: 'json' 35 | yaml: 'yaml' 36 | bash: 'bash' 37 | c: 'c' 38 | java: 'java' 39 | kotlin: 'kotlin' 40 | python: 'py' 'python' 41 | csharp: 'c#' 'csharp' 42 | go: 'go' 43 | sql: 'sql' 44 | } 45 | 46 | @import '~@source/.vuepress/override.styl' 47 | -------------------------------------------------------------------------------- /src/.vuepress/theme/components/Pagination.vue: -------------------------------------------------------------------------------- 1 | 22 | 54 | 59 | -------------------------------------------------------------------------------- /lib/markdown/index.js: -------------------------------------------------------------------------------- 1 | const highlight = require('./highlight') 2 | const highlightLines = require('./highlightLines') 3 | const component = require('./component') 4 | const hoistScriptStyle = require('./hoist') 5 | const convertRouterLink = require('./link') 6 | const containers = require('./containers') 7 | const emoji = require('markdown-it-emoji') 8 | const anchor = require('markdown-it-anchor') 9 | const toc = require('markdown-it-table-of-contents') 10 | const _slugify = require('./slugify') 11 | 12 | module.exports = ({ markdown = {}} = {}) => { 13 | // allow user config slugify 14 | const slugify = markdown.slugify || _slugify 15 | 16 | const md = require('markdown-it')({ 17 | html: true, 18 | highlight 19 | }) 20 | // custom plugins 21 | .use(component) 22 | .use(highlightLines) 23 | .use(convertRouterLink) 24 | .use(hoistScriptStyle) 25 | .use(containers) 26 | 27 | // 3rd party plugins 28 | .use(emoji) 29 | .use(anchor, Object.assign({ 30 | slugify, 31 | permalink: true, 32 | permalinkBefore: true, 33 | permalinkSymbol: '#' 34 | }, markdown.anchor)) 35 | .use(toc, Object.assign({ 36 | slugify, 37 | includeLevel: [2, 3] 38 | }, markdown.toc)) 39 | 40 | // apply user config 41 | if (markdown.config) { 42 | markdown.config(md) 43 | } 44 | 45 | // override render to allow custom plugins return data 46 | const render = md.render 47 | md.render = (...args) => { 48 | md.__data = {} 49 | const html = render.call(md, ...args) 50 | return { 51 | html, 52 | data: md.__data 53 | } 54 | } 55 | 56 | // expose slugify 57 | md.slugify = slugify 58 | 59 | return md 60 | } 61 | -------------------------------------------------------------------------------- /lib/webpack/createServerConfig.js: -------------------------------------------------------------------------------- 1 | module.exports = function createServerConfig (options, cliOptions) { 2 | const fs = require('fs') 3 | const path = require('path') 4 | const WebpackBar = require('webpackbar') 5 | const createBaseConfig = require('./createBaseConfig') 6 | const VueSSRServerPlugin = require('vue-server-renderer/server-plugin') 7 | const CopyPlugin = require('copy-webpack-plugin') 8 | 9 | const config = createBaseConfig(options, cliOptions, true /* isServer */) 10 | const { sourceDir, outDir } = options 11 | 12 | config 13 | .target('node') 14 | .externals([/^vue|vue-router$/]) 15 | .devtool('source-map') 16 | 17 | // no need to minimize server build 18 | config.optimization.minimize(false) 19 | 20 | config 21 | .entry('app') 22 | .add(path.resolve(__dirname, '../app/serverEntry.js')) 23 | 24 | config.output 25 | .filename('server-bundle.js') 26 | .libraryTarget('commonjs2') 27 | 28 | config 29 | .plugin('ssr-server') 30 | .use(VueSSRServerPlugin, [{ 31 | filename: 'manifest/server.json' 32 | }]) 33 | 34 | const publicDir = path.resolve(sourceDir, '.vuepress/public') 35 | if (fs.existsSync(publicDir)) { 36 | config 37 | .plugin('copy') 38 | .use(CopyPlugin, [[ 39 | { from: publicDir, to: outDir } 40 | ]]) 41 | } 42 | 43 | if (!cliOptions.debug) { 44 | config 45 | .plugin('bar') 46 | .use(WebpackBar, [{ 47 | name: 'Server', 48 | color: 'blue', 49 | compiledIn: false 50 | }]) 51 | } 52 | 53 | if (options.siteConfig.chainWebpack) { 54 | options.siteConfig.chainWebpack(config, true /* isServer */) 55 | } 56 | 57 | return config 58 | } 59 | -------------------------------------------------------------------------------- /lib/default-theme/SidebarGroup.vue: -------------------------------------------------------------------------------- 1 | 18 | 19 | 29 | 30 | 64 | -------------------------------------------------------------------------------- /src/.vuepress/theme/Tags.vue: -------------------------------------------------------------------------------- 1 | 32 | 51 | 57 | -------------------------------------------------------------------------------- /src/.vuepress/theme/styles/roboto.styl: -------------------------------------------------------------------------------- 1 | @font-face { 2 | font-family: "Roboto"; 3 | src: local('Roboto Thin'), local('Roboto-Thin'), url($font-roboto + "/Roboto-Thin.woff2") format("woff2"), 4 | url($font-roboto + "/Roboto-Thin.woff") format("woff"), 5 | url($font-roboto + "/Roboto-Thin.ttf") format("truetype"); 6 | 7 | font-weight: 200; 8 | } 9 | @font-face { 10 | font-family: "Roboto"; 11 | src: local('Roboto Light'), local('Roboto-Light'), url($font-roboto + "/Roboto-Light.woff2") format("woff2"), 12 | url($font-roboto + "/Roboto-Light.woff") format("woff"), 13 | url($font-roboto + "/Roboto-Light.ttf") format("truetype"); 14 | font-weight: 300; 15 | } 16 | 17 | @font-face { 18 | font-family: "Roboto"; 19 | src: local('Roboto Regular'), local('Roboto-Regular'), url($font-roboto + "/Roboto-Regular.woff2") format("woff2"), 20 | url($font-roboto + "/Roboto-Regular.woff") format("woff"), 21 | url($font-roboto + "/Roboto-Regular.ttf") format("truetype"); 22 | font-weight: 400; 23 | } 24 | 25 | @font-face { 26 | font-family: "Roboto"; 27 | src: local('Roboto Medium'), local('Roboto-Medium'), url($font-roboto + "/Roboto-Medium.woff2") format("woff2"), 28 | url($font-roboto + "/Roboto-Medium.woff") format("woff"), 29 | url($font-roboto + "/Roboto-Medium.ttf") format("truetype"); 30 | font-weight: 500; 31 | } 32 | 33 | @font-face { 34 | font-family: "Roboto"; 35 | src: local('Roboto Bold'), local('Roboto-Bold'), url($font-roboto + "/Roboto-Bold.woff2") format("woff2"), 36 | url($font-roboto + "/Roboto-Bold.woff") format("woff"), 37 | url($font-roboto + "/Roboto-Bold.ttf") format("truetype"); 38 | font-weight: 700; 39 | } 40 | -------------------------------------------------------------------------------- /src/.vuepress/theme/components/index.js: -------------------------------------------------------------------------------- 1 | import Vuetify from 'vuetify/es5/components/Vuetify' 2 | import VApp from 'vuetify/es5/components/VApp' 3 | import VGrid from 'vuetify/es5/components/VGrid' 4 | import VFooter from 'vuetify/es5/components/VFooter' 5 | import VToolBar from 'vuetify/es5/components/VToolBar' 6 | import VNavDrawer from 'vuetify/es5/components/VNavigationDrawer' 7 | import VTabs from 'vuetify/es5/components/VTabs' 8 | import VMenu from 'vuetify/es5/components/VMenu' 9 | import VList from 'vuetify/es5/components/VList' 10 | import VPagination from 'vuetify/es5/components/VPagination' 11 | import VSubheader from 'vuetify/es5/components/VSubheader' 12 | import VParallax from 'vuetify/es5/components/VParallax' 13 | import VBtn from 'vuetify/es5/components/VBtn' 14 | import VCard from 'vuetify/es5/components/VCard' 15 | import VAvatar from 'vuetify/es5/components/VAvatar' 16 | import VChip from 'vuetify/es5/components/VChip' 17 | import VDivider from 'vuetify/es5/components/VDivider' 18 | import VSnackbar from 'vuetify/es5/components/VSnackbar' 19 | import VProgressLinear from 'vuetify/es5/components/VProgressLinear' 20 | import * as directives from 'vuetify/es5/directives' 21 | 22 | const install = (Vue, theme) => { 23 | Vue.use(Vuetify, { 24 | components: { 25 | VApp, 26 | VGrid, 27 | VFooter, 28 | VToolBar, 29 | VNavDrawer, 30 | VTabs, 31 | VMenu, 32 | VList, 33 | VPagination, 34 | VSubheader, 35 | VParallax, 36 | VBtn, 37 | VCard, 38 | VAvatar, 39 | VChip, 40 | VDivider, 41 | VSnackbar, 42 | VProgressLinear 43 | }, 44 | directives, 45 | theme: theme.colors 46 | // options: { 47 | // } 48 | }) 49 | } 50 | 51 | export default { install } 52 | -------------------------------------------------------------------------------- /lib/markdown/highlightLines.js: -------------------------------------------------------------------------------- 1 | // forked from https://github.com/egoist/markdown-it-highlight-lines 2 | 3 | const RE = /{([\d,-]+)}/ 4 | const wrapperRE = /^
/
 5 | 
 6 | module.exports = md => {
 7 |   const fence = md.renderer.rules.fence
 8 |   md.renderer.rules.fence = (...args) => {
 9 |     const [tokens, idx, options] = args
10 |     const token = tokens[idx]
11 | 
12 |     if (!token.info || !RE.test(token.info)) {
13 |       return fence(...args)
14 |     }
15 | 
16 |     const langName = token.info.replace(RE, '').trim()
17 |     const lineNumbers = RE.exec(token.info)[1]
18 |       .split(',')
19 |       .map(v => v.split('-').map(v => parseInt(v, 10)))
20 | 
21 |     const code = options.highlight
22 |       ? options.highlight(token.content, langName)
23 |       : token.content
24 | 
25 |     const rawCode = code.replace(wrapperRE, '')
26 |     const leadingWrapper = code.match(wrapperRE)[0]
27 | 
28 |     const codeSplits = rawCode.split('\n').map((split, index) => {
29 |       const lineNumber = index + 1
30 |       const inRange = lineNumbers.some(([start, end]) => {
31 |         if (start && end) {
32 |           return lineNumber >= start && lineNumber <= end
33 |         }
34 |         return lineNumber === start
35 |       })
36 |       if (inRange) {
37 |         return {
38 |           code: `${split}`,
39 |           highlighted: true
40 |         }
41 |       }
42 |       return {
43 |         code: split
44 |       }
45 |     })
46 |     let highlightedCode = leadingWrapper
47 |     codeSplits.forEach(split => {
48 |       if (split.highlighted) {
49 |         highlightedCode += split.code
50 |       } else {
51 |         highlightedCode += `${split.code}\n`
52 |       }
53 |     })
54 |     return highlightedCode
55 |   }
56 | }
57 | 


--------------------------------------------------------------------------------
/docs/404.html:
--------------------------------------------------------------------------------
 1 | 
 2 | 
 3 |   
 4 |     
 5 |     
 6 |     VuePress
 7 |     
 8 |     
 9 |   
10 |   
11 |   
12 |   
13 |   
14 |   
15 |   
16 |     
17 |     
18 |     
19 |   
20 |   
21 |     

404

22 | 23 | 24 | 25 | -------------------------------------------------------------------------------- /src/.vuepress/theme/Post.vue: -------------------------------------------------------------------------------- 1 | 45 | 56 | 71 | -------------------------------------------------------------------------------- /lib/default-theme/Navbar.vue: -------------------------------------------------------------------------------- 1 | 21 | 22 | 40 | 41 | 72 | -------------------------------------------------------------------------------- /src/.vuepress/theme/Footer.vue: -------------------------------------------------------------------------------- 1 | 33 | 44 | 74 | -------------------------------------------------------------------------------- /lib/markdown/link.js: -------------------------------------------------------------------------------- 1 | // markdown-it plugin for: 2 | // 1. adding target="_blank" to external links 3 | // 2. converting internal links to 4 | 5 | module.exports = md => { 6 | let hasOpenRouterLink = false 7 | 8 | md.renderer.rules.link_open = (tokens, idx, options, env, self) => { 9 | const token = tokens[idx] 10 | const hrefIndex = token.attrIndex('href') 11 | if (hrefIndex >= 0) { 12 | const link = token.attrs[hrefIndex] 13 | const href = link[1] 14 | const isExternal = /^https?:/.test(href) 15 | const isSourceLink = /(\/|\.md|\.html)(#.*)?$/.test(href) 16 | if (isExternal) { 17 | addAttr(token, 'target', '_blank') 18 | addAttr(token, 'rel', 'noopener noreferrer') 19 | } else if (isSourceLink) { 20 | hasOpenRouterLink = true 21 | tokens[idx] = toRouterLink(token, link) 22 | } 23 | } 24 | return self.renderToken(tokens, idx, options) 25 | } 26 | 27 | function toRouterLink (token, link) { 28 | link[0] = 'to' 29 | let to = link[1] 30 | 31 | // convert link to filename and export it for existence check 32 | const links = md.__data.links || (md.__data.links = []) 33 | links.push(to) 34 | 35 | to = to 36 | .replace(/\.md$/, '.html') 37 | .replace(/\.md(#.*)$/, '.html$1') 38 | // normalize links to README/index 39 | if (/^index|readme\.html/i.test(to)) { 40 | to = '/' 41 | } 42 | // markdown-it encodes the uri 43 | link[1] = decodeURI(to) 44 | return Object.assign({}, token, { 45 | tag: 'router-link' 46 | }) 47 | } 48 | 49 | md.renderer.rules.link_close = (tokens, idx, options, env, self) => { 50 | const token = tokens[idx] 51 | if (hasOpenRouterLink) { 52 | token.tag = 'router-link' 53 | hasOpenRouterLink = false 54 | } 55 | return self.renderToken(tokens, idx, options) 56 | } 57 | } 58 | 59 | function addAttr (token, name, val) { 60 | const targetIndex = token.attrIndex(name) 61 | if (targetIndex < 0) { 62 | token.attrPush([name, val]) 63 | } else { 64 | token.attrs[targetIndex][1] = val 65 | } 66 | } 67 | -------------------------------------------------------------------------------- /lib/default-theme/styles/code.styl: -------------------------------------------------------------------------------- 1 | @require './config' 2 | 3 | .content 4 | code 5 | color lighten($textColor, 20%) 6 | padding 0.25rem 0.5rem 7 | margin 0 8 | font-size 0.85em 9 | background-color rgba(27,31,35,0.05) 10 | border-radius 3px 11 | 12 | .content 13 | pre, pre[class*="language-"] 14 | background-color $codeBgColor 15 | line-height 1.4 16 | border-radius 6px 17 | padding 1.25rem 1.5rem 18 | margin 0.85rem 0 19 | white-space pre-wrap 20 | word-break break-word 21 | overflow auto 22 | position relative 23 | code 24 | color #fff 25 | padding 0 26 | background-color transparent 27 | border-radius 0 28 | &:before 29 | position absolute 30 | top 0.8em 31 | right 1em 32 | font-size 0.75rem 33 | color rgba(255, 255, 255, 0.4) 34 | .highlighted-line 35 | background-color rgba(0, 0, 0, 66%) 36 | display block 37 | margin 0 -1.5rem 38 | padding 0 1.5rem 39 | 40 | pre[class="language-js"], pre[class="language-javascript"] 41 | &:before 42 | content "js" 43 | 44 | pre[class="language-html"], pre[class="language-markup"] 45 | &:before 46 | content "html" 47 | 48 | pre[class="language-markdown"], pre[class="language-md"] 49 | &:before 50 | content "md" 51 | 52 | pre[class="language-vue"]:before 53 | content "vue" 54 | 55 | pre[class="language-css"]:before 56 | content "css" 57 | 58 | pre[class="language-sass"]:before 59 | content "sass" 60 | 61 | pre[class="language-less"]:before 62 | content "less" 63 | 64 | pre[class="language-scss"]:before 65 | content "scss" 66 | 67 | pre[class="language-stylus"]:before 68 | content "stylus" 69 | 70 | pre[class="language-json"]:before 71 | content "json" 72 | 73 | pre[class="language-ruby"]:before 74 | content "rb" 75 | 76 | pre[class="language-python"]:before 77 | content "py" 78 | 79 | pre[class="language-go"]:before 80 | content "go" 81 | 82 | pre[class="language-java"]:before 83 | content "java" 84 | 85 | pre[class="language-c"]:before 86 | content "c" 87 | 88 | pre[class="language-bash"]:before 89 | content "sh" 90 | -------------------------------------------------------------------------------- /src/posts/test-markdown.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: 测试 Markdown 3 | date: 2018-04-29 12:15:53 4 | tags: [markdown] 5 | --- 6 | 7 | 常用 Markdown 语法测试。 8 | 9 | 10 | 11 | ### 1. 斜体和粗体 12 | 13 | 使用 \* 和 \*\* 表示斜体和粗体。 14 | 15 | 示例: 16 | 17 | 这是 _斜体_,这是 **粗体**。 18 | 19 | ### 2. 分级标题 20 | 21 | 使用 === 表示一级标题,使用 --- 表示二级标题。 22 | 23 | 示例: 24 | 25 | ``` 26 | 这是一个一级标题 27 | ============================ 28 | 29 | 这是一个二级标题 30 | -------------------------------------------------- 31 | 32 | ### 这是一个三级标题 33 | ``` 34 | 35 | 你也可以选择在行首加井号表示不同级别的标题 (H1-H6),例如:# H1, ## H2, ### H3,#### H4。 36 | 37 | ### 3. 外链接 38 | 39 | 使用 \[描述](链接地址) 为文字增加外链接。 40 | 41 | 示例: 42 | 43 | 这是去往 [本人博客](/) 的链接。 44 | 45 | ### 4. 无序列表 46 | 47 | 使用 \*,+,- 表示无序列表。 48 | 49 | 示例: 50 | 51 | * 无序列表项 一 52 | * 无序列表项 二 53 | * 无序列表项 三 54 | 55 | ### 5. 有序列表 56 | 57 | 使用数字和点表示有序列表。 58 | 59 | 示例: 60 | 61 | 1. 有序列表项 一 62 | 2. 有序列表项 二 63 | 3. 有序列表项 三 64 | 65 | ### 6. 文字引用 66 | 67 | 使用 > 表示文字引用。 68 | 69 | 示例: 70 | 71 | > 野火烧不尽,春风吹又生。 72 | 73 | ### 7. 行内代码块 74 | 75 | 使用 \`代码` 表示行内代码块。 76 | 77 | 示例: 78 | 79 | 让我们聊聊 `html`。 80 | 81 | ### 8. 代码块 82 | 83 | 使用 四个缩进空格 表示代码块。 84 | 85 | 示例: 86 | 87 | 这是一个代码块,此行左侧有四个不可见的空格。 88 | 89 | ```js{4} 90 | export default { 91 | data () { 92 | return { 93 | msg: 'Highlighted!' 94 | } 95 | } 96 | } 97 | ``` 98 | 99 | ### 9. 插入图像 100 | 101 | 使用 \!\[描述](图片链接地址) 插入图像。 102 | 103 | 示例: 104 | 105 | ![我的头像](~@pub/face.jpg) 106 | 107 | ### 10. 删除线 108 | 109 | 使用 ~~ 表示删除线。 110 | 111 | ~~这是一段错误的文本。~~ 112 | 113 | ### 11. 表格 114 | 115 | | 项目 | 价格 | 数量 | 116 | | ------ | -----: | :--: | 117 | | 计算机 | \$1600 | 5 | 118 | | 手机 | \$12 | 12 | 119 | | 管线 | \$1 | 234 | 120 | 121 | ### 12. 自定义容器 122 | 123 | **Input** 124 | 125 | ``` 126 | ::: tip 提示 127 | This is a tip 128 | ::: 129 | 130 | ::: warning 注意 131 | This is a warning 132 | ::: 133 | 134 | ::: danger 警告 135 | This is a dangerous warning 136 | ::: 137 | ``` 138 | 139 | **Output** 140 | 141 | ::: tip 提示 142 | This is a tip 143 | ::: 144 | 145 | ::: warning 注意 146 | This is a warning 147 | ::: 148 | 149 | ::: danger 警告 150 | This is a dangerous warning 151 | ::: 152 | -------------------------------------------------------------------------------- /lib/app/clientEntry.js: -------------------------------------------------------------------------------- 1 | /* global BASE_URL, GA_ID, ga, SW_ENABLED */ 2 | 3 | import '@temp/polyfill' 4 | import { createApp } from './app' 5 | import { register } from 'register-service-worker' 6 | 7 | const { app, router } = createApp() 8 | 9 | // Google analytics integration 10 | if (process.env.NODE_ENV === 'production' && GA_ID) { 11 | (function (i, s, o, g, r, a, m) { 12 | i['GoogleAnalyticsObject'] = r 13 | i[r] = i[r] || function () { 14 | (i[r].q = i[r].q || []).push(arguments) 15 | } 16 | i[r].l = 1 * new Date() 17 | a = s.createElement(o) 18 | m = s.getElementsByTagName(o)[0] 19 | a.async = 1 20 | a.src = g 21 | m.parentNode.insertBefore(a, m) 22 | })(window, document, 'script', 'https://www.google-analytics.com/analytics.js', 'ga') 23 | 24 | ga('create', GA_ID, 'auto') 25 | ga('send', 'pageview') 26 | 27 | router.afterEach(function (to) { 28 | ga('set', 'page', to.fullPath) 29 | ga('send', 'pageview') 30 | }) 31 | } 32 | 33 | router.onReady(() => { 34 | app.$mount('#app') 35 | 36 | // Register service worker 37 | if (process.env.NODE_ENV === 'production' && 38 | SW_ENABLED && 39 | window.location.protocol === 'https:') { 40 | register(`${BASE_URL}service-worker.js`, { 41 | ready () { 42 | console.log('[vuepress:sw] Service worker is active.') 43 | app.$refs.layout.$emit('sw-ready') 44 | }, 45 | cached () { 46 | console.log('[vuepress:sw] Content has been cached for offline use.') 47 | app.$refs.layout.$emit('sw-cached') 48 | }, 49 | updated () { 50 | console.log('[vuepress:sw] Content updated.') 51 | app.$refs.layout.$emit('sw-updated') 52 | }, 53 | offline () { 54 | console.log('[vuepress:sw] No internet connection found. App is running in offline mode.') 55 | app.$refs.layout.$emit('sw-offline') 56 | }, 57 | error (err) { 58 | console.error('[vuepress:sw] Error during service worker registration:', err) 59 | app.$refs.layout.$emit('sw-error', err) 60 | if (GA_ID) { 61 | ga('send', 'exception', { 62 | exDescription: err.message, 63 | exFatal: false 64 | }) 65 | } 66 | } 67 | }) 68 | } 69 | }) 70 | -------------------------------------------------------------------------------- /lib/webpack/createClientConfig.js: -------------------------------------------------------------------------------- 1 | module.exports = function createClientConfig (options, cliOptions) { 2 | const path = require('path') 3 | const WebpackBar = require('webpackbar') 4 | const createBaseConfig = require('./createBaseConfig') 5 | 6 | const config = createBaseConfig(options, cliOptions) 7 | 8 | config 9 | .entry('app') 10 | .add(path.resolve(__dirname, '../app/clientEntry.js')) 11 | 12 | config.node 13 | .merge({ 14 | // prevent webpack from injecting useless setImmediate polyfill because Vue 15 | // source contains it (although only uses it if it's native). 16 | setImmediate: false, 17 | global: false, 18 | process: false, 19 | // prevent webpack from injecting mocks to Node native modules 20 | // that does not make sense for the client 21 | dgram: 'empty', 22 | fs: 'empty', 23 | net: 'empty', 24 | tls: 'empty', 25 | child_process: 'empty' 26 | }) 27 | 28 | // generate client manifest only during build 29 | if (process.env.NODE_ENV === 'production') { 30 | // This is a temp build of vue-server-renderer/client-plugin. 31 | // TODO Switch back to original after problems are resolved. 32 | // Fixes two things: 33 | // 1. Include CSS in preload files 34 | // 2. filter out useless styles.xxxxx.js chunk from mini-css-extract-plugin 35 | // https://github.com/webpack-contrib/mini-css-extract-plugin/issues/85 36 | config 37 | .plugin('ssr-client') 38 | .use(require('./ClientPlugin'), [{ 39 | filename: 'manifest/client.json' 40 | }]) 41 | 42 | config 43 | .plugin('optimize-css') 44 | .use(require('optimize-css-assets-webpack-plugin'), [{ 45 | canPrint: false, 46 | cssProcessorOptions: { 47 | safe: true, 48 | autoprefixer: { disable: true }, 49 | mergeLonghand: false 50 | } 51 | }]) 52 | } 53 | 54 | if (!cliOptions.debug) { 55 | config 56 | .plugin('bar') 57 | .use(WebpackBar, [{ 58 | name: 'Client', 59 | color: '#41b883', 60 | compiledIn: false 61 | }]) 62 | } 63 | 64 | if (options.siteConfig.chainWebpack) { 65 | options.siteConfig.chainWebpack(config, false /* isServer */) 66 | } 67 | 68 | return config 69 | } 70 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2018 Yusen 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | 23 | The MIT License (MIT) 24 | 25 | Copyright (c) 2018-present, Yuxi (Evan) You 26 | 27 | Permission is hereby granted, free of charge, to any person obtaining a copy 28 | of this software and associated documentation files (the "Software"), to deal 29 | in the Software without restriction, including without limitation the rights 30 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 31 | copies of the Software, and to permit persons to whom the Software is 32 | furnished to do so, subject to the following conditions: 33 | 34 | The above copyright notice and this permission notice shall be included in 35 | all copies or substantial portions of the Software. 36 | 37 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 38 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 39 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 40 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 41 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 42 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 43 | THE SOFTWARE. 44 | -------------------------------------------------------------------------------- /src/.vuepress/theme/libs/blog.js: -------------------------------------------------------------------------------- 1 | import sortBy from 'lodash/sortBy' 2 | import dayjs from 'dayjs' 3 | 4 | const slugReg = /\/([^\/]+).html$/ 5 | function matchSlug(path) { 6 | const arr = path.match(slugReg) 7 | return arr ? arr[1] : null 8 | } 9 | 10 | const install = (Vue, { theme, pages }) => { 11 | // 不依赖已有的内置数据,在这里对 siteData 解析,组装博客需要的数据混入Vue 12 | // Example: { postList: [], posts: {}, tagList: [], tags: { } } 13 | const postList = [] 14 | const posts = {} 15 | const postDir = theme.postDir 16 | 17 | sortBy(pages, page => -new Date(page.frontmatter.date)).forEach(page => { 18 | if (page.path.indexOf(postDir) === 0) { 19 | const slug = matchSlug(page.path) 20 | postList.push(slug) 21 | posts[slug] = { ...page, slug } 22 | } 23 | }) 24 | 25 | const tags = {} 26 | const tagList = [] 27 | postList.forEach(slug => { 28 | const list = posts[slug].frontmatter.tags || [] 29 | list.forEach(tagName => { 30 | if (!tags[tagName]) { 31 | tags[tagName] = [] 32 | tagList.push(tagName) 33 | } 34 | tags[tagName] = tags[tagName].concat(slug) 35 | }) 36 | }) 37 | 38 | Vue.mixin({ 39 | computed: { 40 | $blog() { 41 | return { postList, posts, tags, tagList } 42 | }, 43 | $postNav() { 44 | const slug = matchSlug(this.$route.path) 45 | if (!slug) return 46 | const index = postList.indexOf(slug) 47 | const prev = postList[index - 1] 48 | const next = postList[index + 1] 49 | return { 50 | prev: prev ? posts[prev] : null, 51 | next: next ? posts[next] : null 52 | } 53 | }, 54 | $page() { 55 | // override $page data 56 | const { path, meta } = this.$route 57 | for (let i = 0; i < pages.length; i++) { 58 | const page = pages[i] 59 | const layout = page.frontmatter.layout || 'post' 60 | if (page.path === path || layout === meta.layout) { 61 | return { ...page, path } // rewrite path 62 | } 63 | } 64 | } 65 | } 66 | }) 67 | 68 | const format = theme.format 69 | Vue.filter('date', value => dayjs(value).format(format.date)) 70 | Vue.filter('dateTime', value => dayjs(value).format(format.dateTime)) 71 | } 72 | 73 | export default { install } 74 | -------------------------------------------------------------------------------- /lib/app/dataMixin.js: -------------------------------------------------------------------------------- 1 | import Vue from 'vue' 2 | import { siteData } from './.temp/siteData' 3 | import { findPageForPath } from './util' 4 | 5 | function prepare (siteData) { 6 | siteData.pages.forEach(page => { 7 | if (!page.frontmatter) { 8 | page.frontmatter = {} 9 | } 10 | }) 11 | if (siteData.locales) { 12 | Object.keys(siteData.locales).forEach(path => { 13 | siteData.locales[path].path = path 14 | }) 15 | } 16 | Object.freeze(siteData) 17 | } 18 | 19 | prepare(siteData) 20 | const store = new Vue({ 21 | data: { siteData } 22 | }) 23 | 24 | if (module.hot) { 25 | module.hot.accept('./.temp/siteData', () => { 26 | prepare(siteData) 27 | store.siteData = siteData 28 | }) 29 | } 30 | 31 | export default { 32 | computed: { 33 | $site () { 34 | return store.siteData 35 | }, 36 | $localeConfig () { 37 | const { locales = {}} = this.$site 38 | let targetLang 39 | let defaultLang 40 | for (const path in locales) { 41 | if (path === '/') { 42 | defaultLang = locales[path] 43 | } else if (this.$page.path.indexOf(path) === 0) { 44 | targetLang = locales[path] 45 | } 46 | } 47 | return targetLang || defaultLang || {} 48 | }, 49 | $siteTitle () { 50 | return this.$localeConfig.title || this.$site.title || '' 51 | }, 52 | $title () { 53 | const page = this.$page 54 | const siteTitle = this.$siteTitle 55 | const selfTitle = page.frontmatter.home ? null : ( 56 | page.frontmatter.title || // explicit title 57 | page.title // inferred title 58 | ) 59 | return siteTitle 60 | ? selfTitle 61 | ? (siteTitle + ' | ' + selfTitle) 62 | : siteTitle 63 | : selfTitle || 'VuePress' 64 | }, 65 | $description () { 66 | return this.$page.frontmatter.description || this.$localeConfig.description || this.$site.description || '' 67 | }, 68 | $lang () { 69 | return this.$page.frontmatter.lang || this.$localeConfig.lang || 'en-US' 70 | }, 71 | $localePath () { 72 | return this.$localeConfig.path || '/' 73 | }, 74 | $themeLocaleConfig () { 75 | return (this.$site.themeConfig.locales || {})[this.$localePath] || {} 76 | }, 77 | $page () { 78 | return findPageForPath( 79 | this.$site.pages, 80 | this.$route.path 81 | ) 82 | } 83 | } 84 | } 85 | -------------------------------------------------------------------------------- /lib/util/index.js: -------------------------------------------------------------------------------- 1 | const parseEmojis = str => { 2 | const emojiData = require('markdown-it-emoji/lib/data/full.json') 3 | return str.replace(/:(.+?):/g, (placeholder, key) => emojiData[key] || placeholder) 4 | } 5 | 6 | exports.normalizeHeadTag = tag => { 7 | if (typeof tag === 'string') { 8 | tag = [tag] 9 | } 10 | const tagName = tag[0] 11 | return { 12 | tagName, 13 | attributes: tag[1] || {}, 14 | innerHTML: tag[2] || '', 15 | closeTag: !(tagName === 'meta' || tagName === 'link') 16 | } 17 | } 18 | 19 | exports.applyUserWebpackConfig = function (userConfig, config, isServer) { 20 | const merge = require('webpack-merge') 21 | if (typeof userConfig === 'object') { 22 | return merge(config, userConfig) 23 | } 24 | if (typeof userConfig === 'function') { 25 | const res = userConfig(config, isServer) 26 | if (res && typeof res === 'object') { 27 | return merge(config, res) 28 | } 29 | } 30 | return config 31 | } 32 | 33 | exports.inferTitle = function (frontmatter) { 34 | if (frontmatter.data.home) { 35 | return 'Home' 36 | } 37 | if (frontmatter.data.title) { 38 | return parseEmojis(frontmatter.data.title) 39 | } 40 | const match = frontmatter.content.trim().match(/^#+\s+(.*)/) 41 | if (match) { 42 | return parseEmojis(match[1]) 43 | } 44 | } 45 | 46 | exports.parseFrontmatter = content => { 47 | const matter = require('gray-matter') 48 | const toml = require('toml') 49 | 50 | return matter(content, { 51 | excerpt_separator: '', 52 | engines: { 53 | toml: toml.parse.bind(toml), 54 | excerpt: false 55 | } 56 | }) 57 | } 58 | 59 | const LRU = require('lru-cache') 60 | const cache = LRU({ max: 1000 }) 61 | 62 | exports.extractHeaders = (content, include = [], md) => { 63 | const key = content + include.join(',') 64 | const hit = cache.get(key) 65 | if (hit) { 66 | return hit 67 | } 68 | 69 | const tokens = md.parse(content, {}) 70 | 71 | const res = [] 72 | tokens.forEach((t, i) => { 73 | if (t.type === 'heading_open' && include.includes(t.tag)) { 74 | const title = parseEmojis(tokens[i + 1].content) 75 | const slug = t.attrs.find(([name]) => name === 'id')[1] 76 | res.push({ 77 | level: parseInt(t.tag.slice(1), 10), 78 | title, 79 | slug: slug || md.slugify(title) 80 | }) 81 | } 82 | }) 83 | 84 | cache.set(key, res) 85 | return res 86 | } 87 | -------------------------------------------------------------------------------- /src/.vuepress/theme/components/Share.vue: -------------------------------------------------------------------------------- 1 | 41 | 84 | -------------------------------------------------------------------------------- /lib/app/app.js: -------------------------------------------------------------------------------- 1 | import Vue from 'vue' 2 | import Router from 'vue-router' 3 | import Content from './Content' 4 | import ClientOnly from './ClientOnly' 5 | import dataMixin from './dataMixin' 6 | import NotFound from '@notFound' 7 | import { routes } from '@temp/routes' 8 | import { siteData } from '@temp/siteData' 9 | import enhanceApp from '@temp/enhanceApp' 10 | import themeEnhanceApp from '@temp/themeEnhanceApp' 11 | 12 | // suggest dev server restart on base change 13 | if (module.hot) { 14 | const prevBase = siteData.base 15 | module.hot.accept('./.temp/siteData', () => { 16 | if (siteData.base !== prevBase) { 17 | window.alert( 18 | `[vuepress] Site base has changed. ` + 19 | `Please restart dev server to ensure correct asset paths.` 20 | ) 21 | } 22 | }) 23 | } 24 | 25 | Vue.config.productionTip = false 26 | Vue.use(Router) 27 | // mixin for exposing $site and $page 28 | Vue.mixin(dataMixin) 29 | // component for rendering markdown content and setting title etc. 30 | Vue.component('Content', Content) 31 | // component for client-only content 32 | Vue.component('ClientOnly', ClientOnly) 33 | 34 | // global helper for adding base path to absolute urls 35 | Vue.prototype.$withBase = function (path) { 36 | const base = this.$site.base 37 | if (path.charAt(0) === '/') { 38 | return base + path.slice(1) 39 | } else { 40 | return path 41 | } 42 | } 43 | 44 | // add 404 route 45 | routes.push({ 46 | path: '*', 47 | component: NotFound 48 | }) 49 | 50 | export function createApp () { 51 | const router = new Router({ 52 | base: siteData.base, 53 | mode: 'history', 54 | fallback: false, 55 | routes, 56 | scrollBehavior: (to, from, saved) => { 57 | if (saved) { 58 | return saved 59 | } else if (to.hash) { 60 | return { selector: to.hash } 61 | } else { 62 | return { x: 0, y: 0 } 63 | } 64 | } 65 | }) 66 | 67 | // redirect /foo to /foo/ 68 | router.beforeEach((to, from, next) => { 69 | if (!/(\/|\.html)$/.test(to.path)) { 70 | next(Object.assign({}, to, { 71 | path: to.path + '/' 72 | })) 73 | } else { 74 | next() 75 | } 76 | }) 77 | 78 | const options = {} 79 | 80 | themeEnhanceApp({ Vue, options, router, siteData }) 81 | enhanceApp({ Vue, options, router, siteData }) 82 | 83 | const app = new Vue( 84 | Object.assign(options, { 85 | router, 86 | render (h) { 87 | return h('div', { attrs: { id: 'app' }}, [ 88 | h('router-view', { ref: 'layout' }) 89 | ]) 90 | } 91 | }) 92 | ) 93 | 94 | return { app, router } 95 | } 96 | -------------------------------------------------------------------------------- /src/.vuepress/theme/components/PostCard.vue: -------------------------------------------------------------------------------- 1 | 32 | 68 | 107 | -------------------------------------------------------------------------------- /docs/assets/js/4.7a34dcbf.js: -------------------------------------------------------------------------------- 1 | (window.webpackJsonp=window.webpackJsonp||[]).push([[4],{358:function(t,s,a){"use strict";a.r(s);var e=a(0),n=Object(e.a)({},function(){this.$createElement;this._self._c;return this._m(0)},[function(){var t=this,s=t.$createElement,a=t._self._c||s;return a("div",{staticClass:"content"},[a("p",[t._v("关于前端页面的文字溢出截断的招数已经很常见了。\n通常的实现有,前端css控制、后端字数输出控制或者前端js字数处理等。")]),a("h2",{attrs:{id:"单行文字"}},[a("a",{staticClass:"header-anchor",attrs:{href:"#单行文字","aria-hidden":"true"}},[t._v("#")]),t._v(" 单行文字")]),a("p",[t._v("单行文字的溢出处理很简单,我通常是使用css来控制,在文字末尾加上"),a("code",[t._v("...")]),t._v("。")]),a("pre",{pre:!0,attrs:{class:"language-css"}},[a("code",[a("span",{attrs:{class:"token selector"}},[t._v(".ellipsis")]),t._v(" "),a("span",{attrs:{class:"token punctuation"}},[t._v("{")]),t._v("\n "),a("span",{attrs:{class:"token property"}},[t._v("overflow")]),a("span",{attrs:{class:"token punctuation"}},[t._v(":")]),t._v(" hidden"),a("span",{attrs:{class:"token punctuation"}},[t._v(";")]),t._v("\n "),a("span",{attrs:{class:"token property"}},[t._v("text-overflow")]),a("span",{attrs:{class:"token punctuation"}},[t._v(":")]),t._v(" ellipsis"),a("span",{attrs:{class:"token punctuation"}},[t._v(";")]),t._v("\n "),a("span",{attrs:{class:"token property"}},[t._v("white-space")]),a("span",{attrs:{class:"token punctuation"}},[t._v(":")]),t._v(" nowrap"),a("span",{attrs:{class:"token punctuation"}},[t._v(";")]),t._v("\n"),a("span",{attrs:{class:"token punctuation"}},[t._v("}")]),t._v("\n")])]),a("p",[t._v("给需要文字截断的节点增加一个这样的基础类,然后设置该节点的宽。\n同时注意给未来可能会出现溢出的节点也加上此类,再设置最大宽度"),a("code",[t._v("max-width")]),t._v(",因为很多奇葩用户的输入是你无法掌控的。 - -!")]),a("h2",{attrs:{id:"多行文字"}},[a("a",{staticClass:"header-anchor",attrs:{href:"#多行文字","aria-hidden":"true"}},[t._v("#")]),t._v(" 多行文字")]),a("p",[t._v("我希望在一个的固定高度的容器中,内容超出后,最后一个文字显示"),a("code",[t._v("...")]),t._v("。")]),a("p",[t._v("如法炮制给多行文字的容器添加"),a("code",[t._v("ellipsis")]),t._v("类后,你会发现的确是显示"),a("code",[t._v("...")]),t._v(",不过此时文字是一行的。\n因为在该类中添加了属性"),a("code",[t._v("white-space")]),t._v(",用来定义一个段落如何换行。属性值"),a("code",[t._v("nowrap")]),t._v(":禁止文本换行,除非遇到"),a("code",[t._v("
")]),t._v("。")]),a("p",[t._v("一番思考后,确定了一个方案。")]),a("ol",[a("li",[t._v("给固定高度的容器添加"),a("code",[t._v("overflow: hidden")]),t._v(";")]),a("li",[t._v("给容器添加相对定位;")]),a("li",[t._v("添加伪元素样式,"),a("code",[t._v("content:'...'")]),t._v(",绝对定位,然后位置定位在容器末尾。")])]),a("p",[t._v("伪元素的兼容性为IE8,如果需要兼容IE7的可以使用标签代替。")]),a("h2",{attrs:{id:"带显示全部的多行文字"}},[a("a",{staticClass:"header-anchor",attrs:{href:"#带显示全部的多行文字","aria-hidden":"true"}},[t._v("#")]),t._v(" 带显示全部的多行文字")]),a("p",[t._v("类似 QQ 空间、微信、微博那种。")]),a("p",[t._v("{% jsfiddle imys/wymxhaek/3 html,css,result %}")]),a("p",[t._v("使用 2 个伪元素加 1 个 a 链接实现。\n精妙之处在于使用伪元素遮挡一行文字,让 a 链接位于伪元素之上显示。")])])}],!1,null,null,null);s.default=n.exports}}]); -------------------------------------------------------------------------------- /lib/default-theme/Sidebar.vue: -------------------------------------------------------------------------------- 1 | 19 | 20 | 71 | 72 | 103 | -------------------------------------------------------------------------------- /lib/markdown/component.js: -------------------------------------------------------------------------------- 1 | // Replacing the default htmlBlock rule to allow using custom components at 2 | // root level 3 | 4 | const blockNames = require('markdown-it/lib/common/html_blocks') 5 | const HTML_OPEN_CLOSE_TAG_RE = require('markdown-it/lib/common/html_re').HTML_OPEN_CLOSE_TAG_RE 6 | 7 | // An array of opening and corresponding closing sequences for html tags, 8 | // last argument defines whether it can terminate a paragraph or not 9 | const HTML_SEQUENCES = [ 10 | [/^<(script|pre|style)(?=(\s|>|$))/i, /<\/(script|pre|style)>/i, true], 11 | [/^/, true], 12 | [/^<\?/, /\?>/, true], 13 | [/^/, true], 14 | [/^/, true], 15 | // PascalCase Components 16 | [/^<[A-Z]/, />/, true], 17 | // custom elements with hyphens 18 | [/^<\w+\-/, />/, true], 19 | [new RegExp('^|$))', 'i'), /^$/, true], 20 | [new RegExp(HTML_OPEN_CLOSE_TAG_RE.source + '\\s*$'), /^$/, false] 21 | ] 22 | 23 | module.exports = md => { 24 | md.block.ruler.at('html_block', htmlBlock) 25 | } 26 | 27 | function htmlBlock (state, startLine, endLine, silent) { 28 | let i, nextLine, lineText 29 | let pos = state.bMarks[startLine] + state.tShift[startLine] 30 | let max = state.eMarks[startLine] 31 | 32 | // if it's indented more than 3 spaces, it should be a code block 33 | if (state.sCount[startLine] - state.blkIndent >= 4) { return false } 34 | 35 | if (!state.md.options.html) { return false } 36 | 37 | if (state.src.charCodeAt(pos) !== 0x3C/* < */) { return false } 38 | 39 | lineText = state.src.slice(pos, max) 40 | 41 | for (i = 0; i < HTML_SEQUENCES.length; i++) { 42 | if (HTML_SEQUENCES[i][0].test(lineText)) { break } 43 | } 44 | 45 | if (i === HTML_SEQUENCES.length) { 46 | console.log(lineText) 47 | return false 48 | } 49 | 50 | if (silent) { 51 | // true if this sequence can be a terminator, false otherwise 52 | return HTML_SEQUENCES[i][2] 53 | } 54 | 55 | nextLine = startLine + 1 56 | 57 | // If we are here - we detected HTML block. 58 | // Let's roll down till block end. 59 | if (!HTML_SEQUENCES[i][1].test(lineText)) { 60 | for (; nextLine < endLine; nextLine++) { 61 | if (state.sCount[nextLine] < state.blkIndent) { break } 62 | 63 | pos = state.bMarks[nextLine] + state.tShift[nextLine] 64 | max = state.eMarks[nextLine] 65 | lineText = state.src.slice(pos, max) 66 | 67 | if (HTML_SEQUENCES[i][1].test(lineText)) { 68 | if (lineText.length !== 0) { nextLine++ } 69 | break 70 | } 71 | } 72 | } 73 | 74 | state.line = nextLine 75 | 76 | const token = state.push('html_block', '', 0) 77 | token.map = [startLine, nextLine] 78 | token.content = state.getLines(startLine, nextLine, state.blkIndent, true) 79 | 80 | return true 81 | } 82 | -------------------------------------------------------------------------------- /lib/default-theme/SidebarLink.vue: -------------------------------------------------------------------------------- 1 | 57 | 58 | 89 | -------------------------------------------------------------------------------- /src/.vuepress/theme/SideNav.vue: -------------------------------------------------------------------------------- 1 | 37 | 47 | 117 | -------------------------------------------------------------------------------- /lib/webpack/ClientPlugin.js: -------------------------------------------------------------------------------- 1 | // Temporarily hacked dev build 2 | 3 | var isJS = function (file) { return /\.js(\?[^.]+)?$/.test(file) } 4 | 5 | var isCSS = function (file) { return /\.css(\?[^.]+)?$/.test(file) } 6 | 7 | var onEmit = function (compiler, name, hook) { 8 | if (compiler.hooks) { 9 | // Webpack >= 4.0.0 10 | compiler.hooks.emit.tapAsync(name, hook) 11 | } else { 12 | // Webpack < 4.0.0 13 | compiler.plugin('emit', hook) 14 | } 15 | } 16 | 17 | var hash = require('hash-sum') 18 | var uniq = require('lodash.uniq') 19 | var VueSSRClientPlugin = function VueSSRClientPlugin (options) { 20 | if (options === void 0) options = {} 21 | 22 | this.options = Object.assign({ 23 | filename: 'vue-ssr-client-manifest.json' 24 | }, options) 25 | } 26 | 27 | VueSSRClientPlugin.prototype.apply = function apply (compiler) { 28 | var this$1 = this 29 | 30 | onEmit(compiler, 'vue-client-plugin', function (compilation, cb) { 31 | var stats = compilation.getStats().toJson() 32 | 33 | var allFiles = uniq(stats.assets 34 | .map(function (a) { return a.name })) 35 | // Avoid preloading / injecting the style chunk 36 | .filter(file => !/styles\.\w{8}\.js$/.test(file)) 37 | 38 | var initialFiles = uniq(Object.keys(stats.entrypoints) 39 | .map(function (name) { return stats.entrypoints[name].assets }) 40 | .reduce(function (assets, all) { return all.concat(assets) }, []) 41 | .filter(function (file) { return isJS(file) || isCSS(file) })) 42 | // Avoid preloading / injecting the style chunk 43 | .filter(file => !/styles\.\w{8}\.js$/.test(file)) 44 | 45 | var asyncFiles = allFiles 46 | .filter(function (file) { return isJS(file) || isCSS(file) }) 47 | .filter(function (file) { return initialFiles.indexOf(file) < 0 }) 48 | 49 | var manifest = { 50 | publicPath: stats.publicPath, 51 | all: allFiles, 52 | initial: initialFiles, 53 | async: asyncFiles, 54 | modules: { /* [identifier: string]: Array */ } 55 | } 56 | 57 | var assetModules = stats.modules.filter(function (m) { return m.assets.length }) 58 | var fileToIndex = function (file) { return manifest.all.indexOf(file) } 59 | stats.modules.forEach(function (m) { 60 | // ignore modules duplicated in multiple chunks 61 | if (m.chunks.length === 1) { 62 | var cid = m.chunks[0] 63 | var chunk = stats.chunks.find(function (c) { return c.id === cid }) 64 | if (!chunk || !chunk.files) { 65 | return 66 | } 67 | var id = m.identifier.replace(/\s\w+$/, '') // remove appended hash 68 | var files = manifest.modules[hash(id)] = chunk.files.map(fileToIndex) 69 | // find all asset modules associated with the same chunk 70 | assetModules.forEach(function (m) { 71 | if (m.chunks.some(function (id) { return id === cid })) { 72 | files.push.apply(files, m.assets.map(fileToIndex)) 73 | } 74 | }) 75 | } 76 | }) 77 | 78 | var json = JSON.stringify(manifest, null, 2) 79 | compilation.assets[this$1.options.filename] = { 80 | source: function () { return json }, 81 | size: function () { return json.length } 82 | } 83 | cb() 84 | }) 85 | } 86 | 87 | module.exports = VueSSRClientPlugin 88 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "vuepress-theme-indigo", 3 | "version": "0.0.1", 4 | "description": "vuepress theme indigo.", 5 | "main": "lib/index.js", 6 | "bin": { 7 | "vuepress": "bin/vuepress.js" 8 | }, 9 | "scripts": { 10 | "dev": "node bin/vuepress dev src", 11 | "build": "node bin/vuepress build src", 12 | "lint": "eslint bin lib test", 13 | "start": "yarn dev && start http://localhost:8080" 14 | }, 15 | "repository": { 16 | "type": "git", 17 | "url": "git+https://github.com/yscoder/vuepress-theme-indigo.git" 18 | }, 19 | "keywords": [ 20 | "vuepress-theme", 21 | "VuePress", 22 | "blog-theme", 23 | "Material Design" 24 | ], 25 | "author": "yusen", 26 | "license": "MIT", 27 | "bugs": { 28 | "url": "https://github.com/yscoder/vuepress-theme-indigo/issues" 29 | }, 30 | "homepage": "https://github.com/yscoder/vuepress-theme-indigo#readme", 31 | "dependencies": { 32 | "@fortawesome/fontawesome-free-webfonts": "^1.0.6", 33 | "autoprefixer": "^8.2.0", 34 | "buble": "^0.19.3", 35 | "buble-loader": "^0.5.0", 36 | "chalk": "^2.3.2", 37 | "chokidar": "^2.0.3", 38 | "commander": "^2.15.1", 39 | "connect-history-api-fallback": "^1.5.0", 40 | "copy-webpack-plugin": "^4.5.1", 41 | "css-loader": "^0.28.11", 42 | "dayjs": "^1.5.16", 43 | "diacritics": "^1.3.0", 44 | "docsearch.js": "^2.5.2", 45 | "es6-promise": "^4.2.4", 46 | "escape-html": "^1.0.3", 47 | "file-loader": "^1.1.11", 48 | "fs-extra": "^5.0.0", 49 | "globby": "^8.0.1", 50 | "gray-matter": "^4.0.1", 51 | "js-yaml": "^3.11.0", 52 | "koa-connect": "^2.0.1", 53 | "koa-mount": "^3.0.0", 54 | "koa-static": "^4.0.2", 55 | "loader-utils": "^1.1.0", 56 | "lodash": "^4.17.10", 57 | "lru-cache": "^4.1.2", 58 | "markdown-it": "^8.4.1", 59 | "markdown-it-anchor": "^4.0.0", 60 | "markdown-it-container": "^2.0.0", 61 | "markdown-it-emoji": "^1.4.0", 62 | "markdown-it-table-of-contents": "^0.3.3", 63 | "mini-css-extract-plugin": "^0.4.0", 64 | "nprogress": "^0.2.0", 65 | "object-assign": "^4.1.1", 66 | "optimize-css-assets-webpack-plugin": "^4.0.0", 67 | "portfinder": "^1.0.13", 68 | "postcss-loader": "^2.1.3", 69 | "prismjs": "^1.13.0", 70 | "register-service-worker": "^1.2.0", 71 | "semver": "^5.5.0", 72 | "stylus": "^0.54.5", 73 | "stylus-loader": "^3.0.2", 74 | "toml": "^2.3.3", 75 | "url-loader": "^1.0.1", 76 | "vue": "^2.5.16", 77 | "vue-loader": "^15.0.0-rc.1", 78 | "vue-router": "^3.0.1", 79 | "vue-server-renderer": "^2.5.16", 80 | "vue-template-compiler": "^2.5.16", 81 | "vuepress-html-webpack-plugin": "^3.2.0", 82 | "vuetify": "^1.0.17", 83 | "webpack": "^4.5.0", 84 | "webpack-chain": "^4.6.0", 85 | "webpack-merge": "^4.1.2", 86 | "webpack-node-externals": "^1.7.2", 87 | "webpack-serve": "^0.3.1", 88 | "webpackbar": "^2.6.1", 89 | "workbox-build": "^3.1.0" 90 | }, 91 | "devDependencies": { 92 | "conventional-changelog": "^1.1.23", 93 | "eslint": "^4.19.1", 94 | "eslint-plugin-vue-libs": "^2.1.0", 95 | "lint-staged": "^7.0.4", 96 | "vuepress-theme-vue": "^1.0.0", 97 | "yorkie": "^1.0.3" 98 | }, 99 | "engines": { 100 | "node": ">=8" 101 | }, 102 | "browserslist": [ 103 | ">1%" 104 | ] 105 | } 106 | -------------------------------------------------------------------------------- /src/posts/webpack-use-lodash.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: Webpack按需打包Lodash的几种方式 3 | tags: [Webpack, Lodash] 4 | date: 2016-12-17 01:41:21 5 | description: Webpakck打包优化,按需打包Lodash模块。 6 | --- 7 | 8 | 在数据操作时,Lodash 就是我的弹药库,不管遇到多复杂的数据结构都能用一些函数轻松拆解。 9 | 10 | ES6 中也新增了诸多新的对象函数,一些简单的项目中 ES6 就足够使用了,但还是会有例外的情况引用了少数的 Lodash 函数。一个完整的 Lodash 库,即使是压缩后,现最新版本也有 `71k` 的体积。不能为了吃一口饭而买下一个饭店啊。 11 | 12 | 针对这个问题,其实已经有很多可选方案了。 13 | 14 | 15 | 16 | ## 函数模块 17 | 18 | Lodash 中的每个函数在 NPM 都有一个单独的发布模块。[NPM: results for ‘lodash’](https://www.npmjs.com/search?q=lodash) 19 | 假如你只需要使用`_.isEqual`,那么你只需要安装`lodash.isequal`模块,然后按以下方式引用。 20 | 21 | ```js 22 | var isEqual = require('lodash.isequal') 23 | // or ES6 24 | import isEqual from 'lodash.isequal' 25 | 26 | isEqual([1, 2, 3], [1, 2, 3]) // true 27 | ``` 28 | 29 | ## 全路径引用 30 | 31 | 在你完整安装 Lodash 后,可以按`lodash/函数名`的格式单独引入需要的函数模块。 32 | 33 | ```js 34 | var difference = require('lodash/difference') 35 | // or ES6 36 | import difference from 'lodash/difference' 37 | 38 | difference([1, 2], [1, 3]) // [2] 39 | ``` 40 | 41 | ## 使用插件优化 42 | 43 | 在简单场景下,以上两种方式足以解决问题。而遇到复杂的数据对象时,我们不得不在一个文件中引入多个 Lodash 函数,这样就需要在文件中写多个`require`或`import`相关函数。 44 | 45 | ```js 46 | import remove from 'lodash/remove' 47 | import uniq from 'lodash/uniq' 48 | import invokeMap from 'lodash/invokeMap' 49 | import sortBy from 'lodash/sortBy' 50 | // more... 51 | ``` 52 | 53 | 正写到关键处却因为引入一个函数要拉到文件头部去定义引用而打乱了思路,很不爽! 54 | 55 | 于是我机智的到 Github 去搜索了`webpack`和`lodash`两个关键词的组合,排在首位的 [lodash-webpack-plugin](https://github.com/lodash/lodash-webpack-plugin) 就是为了解决这个问题而生。 56 | 57 | 使用时需要以下模块,其实除了前两个剩下的一般都已安装了: 58 | 59 | ```bash 60 | $ npm i -S lodash-webpack-plugin babel-plugin-lodash babel-core babel-loader babel-preset-es2015 webpack 61 | ``` 62 | 63 | **配置:** 64 | 65 | ```js webpack.config.js 66 | var LodashModuleReplacementPlugin = require('lodash-webpack-plugin') 67 | var webpack = require('webpack') 68 | 69 | module.exports = { 70 | module: { 71 | loaders: [ 72 | { 73 | loader: 'babel', 74 | test: /\.js$/, 75 | exclude: /node_modules/, 76 | query: { 77 | plugins: ['transform-runtime', 'lodash'], 78 | presets: ['es2015'] 79 | } 80 | } 81 | ] 82 | }, 83 | plugins: [new LodashModuleReplacementPlugin(), new webpack.optimize.OccurrenceOrderPlugin(), new webpack.optimize.UglifyJsPlugin()] 84 | } 85 | ``` 86 | 87 | 其中`babel-plugin-lodash`的配置,也就是`plugins: ['lodash']`,并不是一定要在`loaders`中,也可以单独定义`babel`。 88 | 89 | ```js webpack.config.js 90 | var LodashModuleReplacementPlugin = require('lodash-webpack-plugin') 91 | var webpack = require('webpack') 92 | 93 | module.exports = { 94 | module: { 95 | loaders: [ 96 | { 97 | loader: 'babel', 98 | test: /\.js$/, 99 | exclude: /node_modules/ 100 | } 101 | ] 102 | }, 103 | babel: { 104 | presets: ['es2015'], 105 | plugins: ['transform-runtime', 'lodash'] 106 | }, 107 | plugins: [new LodashModuleReplacementPlugin(), new webpack.optimize.OccurrenceOrderPlugin(), new webpack.optimize.UglifyJsPlugin()] 108 | } 109 | ``` 110 | 111 | 又或者是`.babelrc`文件中。 112 | 113 | 以上工作完成了,在每个你需要使用 lodash 函数的文件中只需要引用一次 lodash,即可调用任意函数而不会造成完全打包。 114 | 115 | ```js 116 | import _ from 'lodash' 117 | 118 | _.add(1, 2) // 打包时只会引入这一个函数模块 119 | ``` 120 | 121 | > 注意:必须要使用 ES2015 的模块引用方式才有效。 122 | 123 | ## End 124 | 125 | 以上即是我目前所知道的几种方式,如果哪位朋友有更好的方式(比如只需要全局引入一次),请一定分享与我!😋 126 | -------------------------------------------------------------------------------- /bin/vuepress.js: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env node 2 | 3 | const chalk = require('chalk') 4 | const semver = require('semver') 5 | const requiredVersion = require('../package.json').engines.node 6 | 7 | if (!semver.satisfies(process.version, requiredVersion)) { 8 | console.log(chalk.red( 9 | `\n[vuepress] minimum Node version not met:` + 10 | `\nYou are using Node ${process.version}, but VuePress ` + 11 | `requires Node ${requiredVersion}.\nPlease upgrade your Node version.\n` 12 | )) 13 | process.exit(1) 14 | } 15 | 16 | const path = require('path') 17 | const { dev, build, eject } = require('../lib') 18 | 19 | const program = require('commander') 20 | 21 | program 22 | .version(require('../package.json').version) 23 | .usage(' [options]') 24 | 25 | program 26 | .command('dev [targetDir]') 27 | .description('start development server') 28 | .option('-p, --port ', 'use specified port (default: 8080)') 29 | .option('-h, --host ', 'use specified host (default: 0.0.0.0)') 30 | .action((dir = '.', { host, port }) => { 31 | wrapCommand(dev)(path.resolve(dir), { host, port }) 32 | }) 33 | 34 | program 35 | .command('build [targetDir]') 36 | .description('build dir as static site') 37 | .option('-d, --dest ', 'specify build output dir (default: .vuepress/dist)') 38 | .option('--debug', 'build in development mode for debugging') 39 | .action((dir = '.', { debug, dest }) => { 40 | const outDir = dest ? path.resolve(dest) : null 41 | wrapCommand(build)(path.resolve(dir), { debug, outDir }) 42 | }) 43 | 44 | program 45 | .command('eject [targetDir]') 46 | .description('copy the default theme into .vuepress/theme for customization.') 47 | .action((dir = '.') => { 48 | wrapCommand(eject)(path.resolve(dir)) 49 | }) 50 | 51 | // output help information on unknown commands 52 | program 53 | .arguments('') 54 | .action((cmd) => { 55 | program.outputHelp() 56 | console.log(` ` + chalk.red(`Unknown command ${chalk.yellow(cmd)}.`)) 57 | console.log() 58 | }) 59 | 60 | // add some useful info on help 61 | program.on('--help', () => { 62 | console.log() 63 | console.log(` Run ${chalk.cyan(`vuepress --help`)} for detailed usage of given command.`) 64 | console.log() 65 | }) 66 | 67 | program.commands.forEach(c => c.on('--help', () => console.log())) 68 | 69 | // enhance common error messages 70 | const enhanceErrorMessages = (methodName, log) => { 71 | program.Command.prototype[methodName] = function (...args) { 72 | if (methodName === 'unknownOption' && this._allowUnknownOption) { 73 | return 74 | } 75 | this.outputHelp() 76 | console.log(` ` + chalk.red(log(...args))) 77 | console.log() 78 | process.exit(1) 79 | } 80 | } 81 | 82 | enhanceErrorMessages('missingArgument', argName => { 83 | return `Missing required argument ${chalk.yellow(`<${argName}>`)}.` 84 | }) 85 | 86 | enhanceErrorMessages('unknownOption', optionName => { 87 | return `Unknown option ${chalk.yellow(optionName)}.` 88 | }) 89 | 90 | enhanceErrorMessages('optionMissingArgument', (option, flag) => { 91 | return `Missing required argument for option ${chalk.yellow(option.flags)}` + ( 92 | flag ? `, got ${chalk.yellow(flag)}` : `` 93 | ) 94 | }) 95 | 96 | program.parse(process.argv) 97 | 98 | if (!process.argv.slice(2).length) { 99 | program.outputHelp() 100 | } 101 | 102 | function wrapCommand (fn) { 103 | return (...args) => { 104 | return fn(...args).catch(err => { 105 | console.error(chalk.red(err.stack)) 106 | }) 107 | } 108 | } 109 | -------------------------------------------------------------------------------- /lib/webpack/markdownLoader.js: -------------------------------------------------------------------------------- 1 | const fs = require('fs') 2 | const path = require('path') 3 | const hash = require('hash-sum') 4 | const { EventEmitter } = require('events') 5 | const { getOptions } = require('loader-utils') 6 | const { inferTitle, extractHeaders, parseFrontmatter } = require('../util') 7 | const LRU = require('lru-cache') 8 | 9 | const cache = LRU({ max: 1000 }) 10 | const devCache = LRU({ max: 1000 }) 11 | 12 | module.exports = function (src) { 13 | const isProd = process.env.NODE_ENV === 'production' 14 | const isServer = this.target === 'node' 15 | const { markdown, sourceDir } = getOptions(this) 16 | 17 | // we implement a manual cache here because this loader is chained before 18 | // vue-loader, and will be applied on the same file multiple times when 19 | // selecting the individual blocks. 20 | const file = this.resourcePath 21 | const key = hash(file + src) 22 | const cached = cache.get(key) 23 | if (cached && (isProd || /\?vue/.test(this.resourceQuery))) { 24 | return cached 25 | } 26 | 27 | const frontmatter = parseFrontmatter(src) 28 | const content = frontmatter.content 29 | 30 | if (!isProd && !isServer) { 31 | const inferredTitle = inferTitle(frontmatter) 32 | const headers = extractHeaders(content, ['h2', 'h3'], markdown) 33 | delete frontmatter.content 34 | 35 | // diff frontmatter and title, since they are not going to be part of the 36 | // returned component, changes in frontmatter do not trigger proper updates 37 | const cachedData = devCache.get(file) 38 | if (cachedData && ( 39 | cachedData.inferredTitle !== inferredTitle || 40 | JSON.stringify(cachedData.frontmatter) !== JSON.stringify(frontmatter) || 41 | headersChanged(cachedData.headers, headers) 42 | )) { 43 | // frontmatter changed... need to do a full reload 44 | module.exports.frontmatterEmitter.emit('update') 45 | } 46 | 47 | devCache.set(file, { 48 | headers, 49 | frontmatter, 50 | inferredTitle 51 | }) 52 | } 53 | 54 | // the render method has been augmented to allow plugins to 55 | // register data during render 56 | const { html, data: { hoistedTags, links }} = markdown.render(content) 57 | 58 | // check if relative links are valid 59 | links && links.forEach(link => { 60 | const shortname = link 61 | .replace(/#[\w-]*$/, '') 62 | .replace(/\.html$/, '.md') 63 | const filename = shortname 64 | .replace(/\/$/, '/README.md') 65 | .replace(/^\//, sourceDir + '/') 66 | const altname = shortname 67 | .replace(/\/$/, '/index.md') 68 | .replace(/^\//, sourceDir + '/') 69 | const dir = path.dirname(this.resourcePath) 70 | const file = path.resolve(dir, filename) 71 | const altfile = altname !== filename ? path.resolve(dir, altname) : null 72 | if (!fs.existsSync(file) && (altfile && !fs.existsSync(altfile))) { 73 | this.emitWarning( 74 | new Error( 75 | `\nFile for relative link "${link}" does not exist.\n` + 76 | `(Resolved file: ${file})\n` 77 | ) 78 | ) 79 | } 80 | }) 81 | 82 | const res = ( 83 | `\n` + 86 | (hoistedTags || []).join('\n') 87 | ) 88 | cache.set(key, res) 89 | return res 90 | } 91 | 92 | function headersChanged (a, b) { 93 | if (a.length !== b.length) return true 94 | return a.some((h, i) => ( 95 | h.title !== b[i].title || 96 | h.level !== b[i].level 97 | )) 98 | } 99 | 100 | module.exports.frontmatterEmitter = new EventEmitter() 101 | -------------------------------------------------------------------------------- /src/.vuepress/config.js: -------------------------------------------------------------------------------- 1 | const path = require('path') 2 | const nodeExternals = require('webpack-node-externals') 3 | 4 | const resolve = pathName => path.join(__dirname, pathName) 5 | const isProd = process.env.NODE_ENV === 'production' 6 | 7 | module.exports = { 8 | base: isProd ? '/vuepress-theme-indigo/' : '/', 9 | dest: 'docs', 10 | title: "Yusen's Blog", 11 | description: '王昱森的博客。无所谓做什么,只要是当前最感兴趣的事!随心、随性、随缘!', 12 | head: [ 13 | ['link', { rel: 'shortcut icon', href: '/favicon.ico' }], 14 | ['link', { rel: 'manifest', href: '/manifest.json' }], 15 | ['meta', { name: 'theme-color', content: '#3F51B5' }], 16 | ['meta', { name: 'apple-mobile-web-app-capable', content: 'yes' }], 17 | ['meta', { name: 'apple-mobile-web-app-status-bar-style', content: 'black' }], 18 | ['link', { rel: 'apple-touch-icon', href: '/icons/192.png' }], 19 | // ['link', { rel: 'mask-icon', href: '/icons/safari-pinned-tab.svg', color: '#3eaf7c' }], 20 | ['meta', { name: 'msapplication-TileImage', content: '/icons/192.png' }], 21 | ['meta', { name: 'msapplication-TileColor', content: '#3F51B5' }] 22 | ], 23 | serviceWorker: true, 24 | theme: '', 25 | locales: { 26 | '/': { 27 | lang: 'zh-CN', 28 | title: "Yusen's Blog", 29 | description: '王昱森的博客。无所谓做什么,只要是当前最感兴趣的事!随心、随性、随缘!' 30 | } 31 | }, 32 | configureWebpack: (config, isServer) => { 33 | const myConfig = { 34 | resolve: { 35 | alias: { 36 | '@pub': resolve('./public') 37 | } 38 | }, 39 | module: { 40 | rules: [{ 41 | test: /vuetify.+\.js$/, 42 | loader: resolve('./ignoreStylus'), 43 | }] 44 | } 45 | } 46 | if (isServer) { 47 | myConfig.externals = nodeExternals({ 48 | whitelist: [/vuetify/, /fortawesome/, /prismjs/] 49 | }) 50 | } 51 | return myConfig 52 | }, 53 | themeConfig: { 54 | lang: 'zh-CN', 55 | postDir: '/posts', 56 | subTitle: '学习弯道超车的技巧!', 57 | author: '王昱森', 58 | email: 'iyusen@foxmail.com', 59 | since: 2015, 60 | avatar: '/face.jpg', 61 | avatarLink: '/', 62 | menus: [ 63 | // icons by https://fontawesome.com/icons 64 | { 65 | text: 'Home', 66 | icon: 'fa fa-home', 67 | url: '/' 68 | }, 69 | { 70 | text: 'Tags', 71 | icon: 'fa fa-tag', 72 | url: '/tags' 73 | }, 74 | { 75 | text: 'Github', 76 | icon: 'fab fa-github', 77 | url: 'https://github.com/yscoder', 78 | external: true 79 | }, 80 | { 81 | text: 'Weibo', 82 | icon: 'fab fa-weibo', 83 | url: 'https://www.weibo.com/ysweb', 84 | external: true 85 | }, 86 | { 87 | text: 'About', 88 | icon: 'fa fa-user-secret', 89 | url: '/about' 90 | } 91 | ], 92 | socials: ['Weibo', 'QQ', 'Facebook', 'Twitter', 'GooglePlus'], 93 | colors: { 94 | // generate by https://vuetifyjs.com/zh-Hans/theme-generator 95 | primary: '#3F51B5', 96 | secondary: '#6d6d6d', 97 | accent: '#E91E63', 98 | error: '#f44336', 99 | warning: '#FFC107', 100 | info: '#00B8D4', 101 | success: '#4caf50' 102 | }, 103 | format: { 104 | date: 'YYYY年MM月DD日', 105 | dateTime: 'YYYY年MM月DD日 HH:mm:ss' 106 | }, 107 | pagination: { 108 | path: '/page/:pageNum', 109 | pageSize: 5 110 | }, 111 | tags: { 112 | path: '/tags/:tagName' 113 | }, 114 | categories: { 115 | path: '/categories/:category' 116 | } 117 | } 118 | } 119 | -------------------------------------------------------------------------------- /lib/default-theme/Home.vue: -------------------------------------------------------------------------------- 1 | 25 | 26 | 44 | 45 | 135 | -------------------------------------------------------------------------------- /src/posts/write-good-front-end-component.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: 编写良好的前端组件 3 | tags: [前端, 组件, Vue, React] 4 | date: 2017-03-17 10:22:38 5 | description: 编写良好的前端组件 6 | --- 7 | Vue 和 React 的大红大火,带来的是组件化和数据驱动的开发方式。Demo 很美好,但如果没有一定的实际开发经验积累,总是能把一个功能模块写成浆糊。 8 | 依托于 Webpack 等构建工具,使得前端代码具备了后端编程语言的代码组织能力,摆脱了传统的「一泻而下」式的代码编写。至此,作为前端也该对自己的代码有更高的要求。 9 | 10 | 11 | ## 组件职责划分 12 | 13 | > 一个组件只做一件事,基于功能做好职责划分。 14 | 15 | ### 无状态组件 16 | 17 | 公司用的是 Vue,最近又接触了下 React。 18 | 对比来说,React 由于 jsx 式(js和html混合)的写法,加上构建工具的模块化管理,一个文件中可以有多个组件。还支持纯函数式的**无状态组件**,只是单纯的接受数据渲染 DOM,没有生命周期等额外的概念。 19 | 20 | ![无状态组件](http://static.imys.net/no-status-component.jpg) 21 | 22 | ```js React 23 | // 无状态组件 24 | const noStatus = props =>

{props.title}

25 | ``` 26 | 27 | 看起来就像一个简单的模版渲染过程。 28 | 29 | Vue 中没有**无状态组件**的概念,但实际上也存在类似功能的组件形式。比如图标组件,只接收 `props` 渲染模版,不做多余的动作。 30 | 31 | ```html Vue 32 | 35 | 42 | ``` 43 | 44 | ### 端对端组件 45 | 46 | 端对端组件指的是不需要依赖外部给予,自身就可以负责从数据获取到展示过程的组件。 47 | 这类组件在业务开发中也很常见,比如公共的分类选择器。由于到多处调用,如果每次用的时候都由外部请求数据在调用组件展示,那么这个请求数据的代码显然是个重复的逻辑,索性直接就写入到组件内部了。 48 | 49 | ![端对端组件](http://static.imys.net/end-to-end-component.jpg) 50 | 51 | > 当然端对端组件也有缺陷。就是每次调用不管数据有没有变化,都会重新请求,造成冗余。如何改善,那又是另一个话题了。这篇文章中有提到:[徐飞:复杂单页应用的数据层设计](https://github.com/xufei/blog/issues) 52 | 53 | ### UI组件 54 | 55 | UI 组件指的是界面扩展类组件,比如:输入框、表格、树、下拉框等。像 Element、Vux 等组件库均属于此类组件。 56 | 57 | ![UI组件](http://static.imys.net/ui-component.jpg) 58 | 59 | 此类组件的特点是:复用性强,只通过 `props`、`events` 和 `slots` 等组件接口与外部通信。 60 | 更像是一个对 HTML 的扩展标签。 61 | 62 | ### 业务组件 63 | 64 | 业务组件通常是根据最小业务状态抽象而出,有些业务组件也具有一定的复用性,但大多数是一次性组件。 65 | 66 | ![业务组件](http://static.imys.net/service-component.jpg) 67 | 68 | 之前提到的组件数据或自给自足(端对端组件),或来自 `props`,那么业务组件的数据呢? 69 | 70 | 1. props 71 | 2. global state 72 | 73 | 只能是以上两种了,如果还是组件内部去请求数据,那么就还是属于端对端组件了。 74 | 75 | ### 容器组件 76 | 77 | 这类组件就是一个盒子,一般当作一个业务子模块的入口,比如一个路由指向的组件。 78 | 79 | ![容器组件](http://static.imys.net/container-component.jpg) 80 | 81 | 通常是这种形式: 82 | 83 | ```html 84 |
85 | 86 | 87 | 88 |
89 | ``` 90 | 91 | * 容器组件内的子组件通常具有业务或数据依赖关系。 92 | * 如果没有使用全局状态管理,那么容器组件就是负责通过 `props` 分发数据到各个子组件,在通过 `events` 处理各个子组件的业务响应。此时容器组件需要做数据请求工作。 93 | * 如果使用了全局状态管理,那么容器内部的业务组件可以自行调用全局状态处理业务。但并不是说此时容器组件什么都不用干了。即使不需要请求数据,还是有许多组件间或一个业务模块内的诸多统筹工作要做。 94 | 95 | 把上面的各类组件组装到一起就组成一个业务模块。 96 | 97 | ![业务模块](http://static.imys.net/module-and-components.jpg) 98 | 99 | ## 组件设计原则 100 | 101 | ### 尽可能的减少状态 102 | 103 | 1. 如果一个数据可以由另一个 state 变换得到,那么这个数据就不是一个 state。只需要写一个变换的处理函数,在 Vue 中可以使用计算属性。 104 | 2. 如果你的 state 是一个数组,而模版最外层是渲染这个数组,那么你需要做的事是把渲染的项作为一个组件,只接受一个单级对象形式的数据,由外部决定这个组件的展示次数。 105 | 3. 如果一个数据是固定的,不会变化的常量,那么这个数据就如同 HTML 固定的站点标题一样,写死或作为全局配置属性等,不属于 state。 106 | 4. 如果一个数据需要从外部得到,它应该属于 props。 107 | 5. 如果组件和兄弟组件拥有相同的 state,那么这个 state 应该放到更高的层级中,使用 props 传递到两个组件中。 108 | 109 | ### 合理的依赖关系 110 | 111 | 1. 父组件不依赖子组件。要做到当我们把子组件删除后,只是丢失了一个功能,或一个模块等,而不会造成父组件及兄弟组件功能异常。 112 | 2. 子组件基于父组件传递 props 作出个性化展示。 113 | 114 | ### 扁平化参数 115 | 116 | 像 HTML 原生元素那样,只接受原始类型(字符串、数值、布尔值和函数)作为属性,避免复杂的对象。当然,数据除外。 117 | 118 | ```html 119 | 120 | 125 | 126 | 127 | 128 | 129 | ``` 130 | 131 | ### 良好的接口设计 132 | 133 | 1. 把组件内部可以完成的工作做到极致。虽然提倡拥抱变化,但接口不是越多越好。 134 | 2. 如果常量变为 props 能应对更多的场景,那么就可以作为 props。原有的常量可作为默认值。 135 | 3. 如果组件不能提供调用者所需求的功能,那么这个组件的接口还不够完善。 136 | 4. 如果需要为了某一调用者编写大量特定需求的代码,那么可以考虑通过扩展等方式构建一个新的组件。 137 | 5. 保证组件的属性和事件足够的给大多数的组件使用。 138 | 139 | ## End 140 | 141 | 设计模式六大原则在组件设计中也有适用的地方。 142 | -------------------------------------------------------------------------------- /lib/default-theme/Page.vue: -------------------------------------------------------------------------------- 1 | 25 | 26 | 117 | 118 | 141 | -------------------------------------------------------------------------------- /lib/default-theme/styles/theme.styl: -------------------------------------------------------------------------------- 1 | @require './config' 2 | @require './nprogress' 3 | @require './code' 4 | @require './custom-blocks' 5 | @require './arrow' 6 | 7 | html, body 8 | padding 0 9 | margin 0 10 | 11 | body 12 | font-family -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Oxygen, Ubuntu, Cantarell, "Fira Sans", "Droid Sans", "Helvetica Neue", sans-serif 13 | -webkit-font-smoothing antialiased 14 | -moz-osx-font-smoothing grayscale 15 | font-size 16px 16 | color $textColor 17 | 18 | .page 19 | padding-left $sidebarWidth 20 | 21 | .navbar 22 | position fixed 23 | z-index 20 24 | top 0 25 | left 0 26 | right 0 27 | height $navbarHeight 28 | background-color #fff 29 | box-sizing border-box 30 | border-bottom 1px solid $borderColor 31 | 32 | .sidebar-mask 33 | position fixed 34 | z-index 9 35 | top 0 36 | left 0 37 | width 100vw 38 | height 100vh 39 | display none 40 | 41 | .sidebar 42 | font-size 15px 43 | background-color #fff 44 | width $sidebarWidth 45 | position fixed 46 | z-index 10 47 | margin 0 48 | top $navbarHeight 49 | left 0 50 | bottom 0 51 | box-sizing border-box 52 | border-right 1px solid $borderColor 53 | overflow-y auto 54 | 55 | .content:not(.custom) 56 | max-width $contentWidth 57 | margin 0 auto 58 | padding 2rem 2.5rem 59 | > *:first-child 60 | margin-top $navbarHeight 61 | a:hover 62 | text-decoration underline 63 | p.demo 64 | padding 1rem 1.5rem 65 | border 1px solid #ddd 66 | border-radius 4px 67 | img 68 | max-width 100% 69 | 70 | .content.custom 71 | padding 0 72 | margin 0 73 | 74 | a 75 | font-weight 500 76 | color $accentColor 77 | text-decoration none 78 | 79 | p a code 80 | font-weight 400 81 | color $accentColor 82 | 83 | kbd 84 | background #eee 85 | border solid 0.15rem #ddd 86 | border-bottom solid 0.25rem #ddd 87 | border-radius 0.15rem 88 | padding 0 0.15em 89 | 90 | blockquote 91 | font-size 1.2rem 92 | color #999 93 | border-left .25rem solid #dfe2e5 94 | margin-left 0 95 | padding-left 1rem 96 | 97 | ul, ol 98 | padding-left 1.2em 99 | 100 | strong 101 | font-weight 600 102 | 103 | h1, h2, h3, h4, h5, h6 104 | font-weight 600 105 | line-height 1.25 106 | .content:not(.custom) > & 107 | margin-top (0.5rem - $navbarHeight) 108 | padding-top ($navbarHeight + 1rem) 109 | margin-bottom 0 110 | &:first-child 111 | margin-top -1.5rem 112 | margin-bottom 1rem 113 | + p, + pre, + .custom-block 114 | margin-top 2rem 115 | &:hover .header-anchor 116 | opacity: 1 117 | 118 | h1 119 | font-size 2.2rem 120 | 121 | h2 122 | font-size 1.65rem 123 | padding-bottom .3rem 124 | border-bottom 1px solid $borderColor 125 | 126 | h3 127 | font-size 1.35rem 128 | 129 | a.header-anchor 130 | font-size 0.85em 131 | float left 132 | margin-left -0.87em 133 | padding-right 0.23em 134 | margin-top 0.125em 135 | opacity 0 136 | &:hover 137 | text-decoration none 138 | 139 | code, kbd 140 | font-family source-code-pro, Menlo, Monaco, Consolas, "Courier New", monospace 141 | 142 | p, ul, ol 143 | line-height 1.7 144 | 145 | hr 146 | border 0 147 | border-top 1px solid $borderColor 148 | 149 | table 150 | border-collapse collapse 151 | margin 1rem 0 152 | 153 | tr 154 | border-top 1px solid #dfe2e5 155 | &:nth-child(2n) 156 | background-color #f6f8fa 157 | 158 | th, td 159 | border 1px solid #dfe2e5 160 | padding .6em 1em 161 | 162 | .custom-layout 163 | padding-top $navbarHeight 164 | 165 | .theme-container 166 | &.sidebar-open 167 | .sidebar-mask 168 | display: block 169 | &.no-navbar 170 | .content:not(.custom) 171 | h1, h2, h3, h4, h5, h6 172 | margin-top 1.5rem 173 | padding-top 0 174 | 175 | 176 | @media (min-width: ($MQMobile + 1px)) 177 | .theme-container.no-sidebar 178 | .sidebar 179 | display none 180 | .page 181 | padding-left 0 182 | 183 | @require './mobile.styl' 184 | -------------------------------------------------------------------------------- /src/.vuepress/theme/styles/content.styl: -------------------------------------------------------------------------------- 1 | code, kbd, pre, samp 2 | font-family: $font-code 3 | 4 | .content.custom { 5 | display: block; 6 | min-height: 150px; 7 | font-size: 16px; 8 | line-height: 1.8; 9 | color: $content-color; 10 | 11 | p { 12 | word-wrap: break-word; 13 | } 14 | 15 | h1, h2, h3, h4, h5, h6, ol, ul, pre, table, figure { 16 | margin-bottom: 1.2em 17 | } 18 | 19 | h1, h2, h3, h4, h5, h6 { 20 | padding-top: 3em; 21 | margin-top: -1.8em; 22 | text-transform: capitalize; 23 | 24 | .header-anchor { 25 | margin-left: -1em; 26 | padding-right: 4px; 27 | color: $light-primary-color; 28 | opacity: 0; 29 | text-decoration: none; 30 | } 31 | 32 | &:hover { 33 | .header-anchor { 34 | opacity: 1; 35 | } 36 | } 37 | } 38 | 39 | a { 40 | position relative 41 | text-decoration none 42 | &:after { 43 | content '' 44 | position absolute 45 | left 0 46 | bottom 0 47 | height 1px 48 | width 100% 49 | background rgba($primary-color, .2) 50 | transition height 200ms ease-in 51 | } 52 | &:hover:after { 53 | height 100% 54 | } 55 | } 56 | 57 | blockquote { 58 | margin 2em 0 2em 1em 59 | padding 4px 24px 60 | font-style italic 61 | color $gray-color 62 | border-left .375em solid $primary-color 63 | p { 64 | margin-top: 0 65 | &:last-child { 66 | margin-bottom: 0 67 | } 68 | } 69 | footer { 70 | font-size 80% 71 | } 72 | } 73 | 74 | strong { 75 | font-weight: 600; 76 | margin: 0 0.2em; 77 | } 78 | 79 | code, kbd { 80 | display: inline; 81 | margin: 0 0.3em; 82 | padding: .125em .25em; 83 | box-shadow: none; 84 | } 85 | 86 | pre, 87 | pre[class*="language-"] { 88 | margin-left: -1.75em; 89 | margin-right: -1.75em; 90 | padding: 1.25em 1.75em; 91 | white-space: pre-wrap; 92 | word-break: break-word; 93 | line-height: 1.6; 94 | background: #3e3e3e; 95 | } 96 | pre[class*='language-'] { 97 | position relative 98 | &:before { 99 | position: absolute; 100 | top: 0.8em; 101 | right: 1em; 102 | font-size: .86em; 103 | color: rgba(255, 255, 255, 0.7); 104 | } 105 | } 106 | 107 | pre { 108 | code { 109 | margin: 0; 110 | padding: 0; 111 | background-color: transparent; 112 | font-size: .86em; 113 | font-weight: 500; 114 | color: rgba(255, 255, 255, 0.8); 115 | 116 | &:before, &:after { 117 | content: ''; 118 | letter-spacing: 0; 119 | } 120 | } 121 | } 122 | 123 | ol, ul { 124 | padding-left: 2em; 125 | } 126 | 127 | table { 128 | width: 100%; 129 | border: 1px solid #dedede; 130 | border-collapse: collapse; 131 | border-spacing: 0; 132 | 133 | thead { 134 | tr { 135 | background: #f8f8f8; 136 | } 137 | } 138 | 139 | tbody { 140 | tr { 141 | &:hover { 142 | background: #efefef; 143 | } 144 | } 145 | } 146 | 147 | td, th { 148 | border: 1px solid #dedede; 149 | padding: .375em .75em; 150 | } 151 | } 152 | 153 | video, audio { 154 | max-width: 100%; 155 | } 156 | 157 | .highlighted-line { 158 | display: block; 159 | margin: 0 -1.5rem; 160 | padding: 0 1.5rem; 161 | background-color: rgba(0, 0, 0, 66%); 162 | } 163 | 164 | *:first-child { 165 | margin-top 0 166 | } 167 | } 168 | 169 | for type, exts in $code-languages 170 | pre 171 | &.language-{exts[0]}:before 172 | content type 173 | if exts[1] 174 | &.language-{exts[1]}:before 175 | content type 176 | 177 | .custom-block 178 | margin-bottom 1.2em 179 | padding 1em 1.5em 0.2em 180 | border-left-width .375em 181 | border-left-style solid 182 | border-radius 4px 183 | &-title 184 | font-weight 600 185 | for type, color in $custom-blocks 186 | &.{type} 187 | color darken(color, 20%) 188 | border-color color!important 189 | background lighten(color, 90%)!important 190 | -------------------------------------------------------------------------------- /lib/default-theme/AlgoliaSearchBox.vue: -------------------------------------------------------------------------------- 1 | 6 | 7 | 35 | 36 | 132 | -------------------------------------------------------------------------------- /lib/default-theme/NavLinks.vue: -------------------------------------------------------------------------------- 1 | 22 | 23 | 100 | 101 | 134 | -------------------------------------------------------------------------------- /src/.vuepress/theme/Layout.vue: -------------------------------------------------------------------------------- 1 | 35 | 133 | 134 | 135 | 136 | 137 | 138 | -------------------------------------------------------------------------------- /lib/default-theme/DropdownLink.vue: -------------------------------------------------------------------------------- 1 | 28 | 29 | 53 | 54 | 158 | -------------------------------------------------------------------------------- /lib/dev.js: -------------------------------------------------------------------------------- 1 | module.exports = async function dev (sourceDir, cliOptions = {}) { 2 | const fs = require('fs') 3 | const path = require('path') 4 | const chalk = require('chalk') 5 | const webpack = require('webpack') 6 | const chokidar = require('chokidar') 7 | const serve = require('webpack-serve') 8 | const convert = require('koa-connect') 9 | const mount = require('koa-mount') 10 | const serveStatic = require('koa-static') 11 | const history = require('connect-history-api-fallback') 12 | const portfinder = require('portfinder') 13 | 14 | const prepare = require('./prepare') 15 | const HeadPlugin = require('./webpack/HeadPlugin') 16 | const createClientConfig = require('./webpack/createClientConfig') 17 | const { applyUserWebpackConfig } = require('./util') 18 | const { frontmatterEmitter } = require('./webpack/markdownLoader') 19 | 20 | process.stdout.write('Extracting site metadata...') 21 | const options = await prepare(sourceDir) 22 | 23 | // setup watchers to update options and dynamically generated files 24 | const update = () => { 25 | prepare(sourceDir).catch(err => { 26 | console.error(chalk.red(err.stack)) 27 | }) 28 | } 29 | 30 | // watch add/remove of files 31 | const pagesWatcher = chokidar.watch([ 32 | path.join(sourceDir, '**/*.md'), 33 | path.join(sourceDir, '.vuepress/components/**/*.vue') 34 | ], { 35 | ignored: '.vuepress/**/*.md', 36 | ignoreInitial: true 37 | }) 38 | pagesWatcher.on('add', update) 39 | pagesWatcher.on('unlink', update) 40 | pagesWatcher.on('addDir', update) 41 | pagesWatcher.on('unlinkDir', update) 42 | 43 | // watch config file 44 | const configWatcher = chokidar.watch([ 45 | path.join(sourceDir, '.vuepress/config.js') 46 | ], { ignoreInitial: true }) 47 | configWatcher.on('change', update) 48 | 49 | // also listen for frontmatter changes from markdown files 50 | frontmatterEmitter.on('update', update) 51 | 52 | // resolve webpack config 53 | let config = createClientConfig(options, cliOptions) 54 | 55 | config 56 | .plugin('html') 57 | // using a fork of html-webpack-plugin to avoid it requiring webpack 58 | // internals from an incompatible version. 59 | .use(require('vuepress-html-webpack-plugin'), [{ 60 | template: path.resolve(__dirname, 'app/index.dev.html') 61 | }]) 62 | 63 | config 64 | .plugin('site-data') 65 | .use(HeadPlugin, [{ 66 | tags: options.siteConfig.head || [] 67 | }]) 68 | 69 | config = config.toConfig() 70 | const userConfig = options.siteConfig.configureWebpack 71 | if (userConfig) { 72 | config = applyUserWebpackConfig(userConfig, config, false /* isServer */) 73 | } 74 | 75 | const compiler = webpack(config) 76 | // webpack-serve hot updates doesn't work properly over 0.0.0.0 on Windows, 77 | // but localhost does not allow visiting over network :/ 78 | const defaultHost = process.platform === 'win32' ? 'localhost' : '0.0.0.0' 79 | const host = cliOptions.host || options.siteConfig.host || defaultHost 80 | const displayHost = host === defaultHost && process.platform !== 'win32' 81 | ? 'localhost' 82 | : host 83 | portfinder.basePort = cliOptions.port || options.siteConfig.port || 8080 84 | const port = await portfinder.getPortPromise() 85 | 86 | let isFirst = true 87 | compiler.hooks.done.tap('vuepress', () => { 88 | if (isFirst) { 89 | isFirst = false 90 | console.log( 91 | `\n VuePress dev server listening at ${ 92 | chalk.cyan(`http://${displayHost}:${port}${options.publicPath}`) 93 | }\n` 94 | ) 95 | } else { 96 | const time = new Date().toTimeString().match(/^[\d:]+/)[0] 97 | console.log(` ${chalk.gray(`[${time}]`)} ${chalk.green('✔')} successfully compiled.`) 98 | } 99 | }) 100 | 101 | const nonExistentDir = path.resolve(__dirname, 'non-existent') 102 | await serve({ 103 | // avoid project cwd from being served. Otherwise if the user has index.html 104 | // in cwd it would break the server 105 | content: [nonExistentDir], 106 | compiler, 107 | host, 108 | dev: { logLevel: 'warn' }, 109 | hot: { logLevel: 'error' }, 110 | logLevel: 'error', 111 | port, 112 | add: app => { 113 | const userPublic = path.resolve(sourceDir, '.vuepress/public') 114 | // respect base when serving static files... 115 | if (fs.existsSync(userPublic)) { 116 | app.use(mount(options.publicPath, serveStatic(userPublic))) 117 | } 118 | 119 | app.use(convert(history({ 120 | rewrites: [ 121 | { from: /\.html$/, to: '/' } 122 | ] 123 | }))) 124 | } 125 | }) 126 | } 127 | -------------------------------------------------------------------------------- /src/posts/cursor-offset-at-input.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: JavaScript 获取输入时的光标位置及场景问题 3 | date: 2016-11-25 10:02:01 4 | tags: [JavaScript, Range, Selection] 5 | description: JavaScript 如何获取输入时的光标位置,在光标处插入或替换内容 6 | --- 7 | 8 | ## 前言 9 | 10 | 在输入编辑的业务场景中,可能会需要在光标当前的位置或附近显示提示选项。比如社交评论中的`@user`功能,要确保提示的用户列表总是出现在`@`字符右下方,又或者是在自定义编辑器中 autocomplete 语法提示,都需要获取光标当前的位置作为参照点。 11 | 12 | 13 | 14 | ## 两种位置 15 | 16 | 对于 WEB 开发来讲,当我们提到某某元素的位置,通常是指这个元素相对于父级或文档的像素单位坐标。而对于输入框中光标,就有了额外的区分。 17 | 18 | ### 相对于内容 19 | 20 | 相对于内容,光标位于第几个字符之后,姑且称之为**字符位置**吧。 21 | 22 | ### 相对于 UI 23 | 24 | 相对于 UI,也就是跟普通页面元素一样的**像素位置**了。 25 | 26 | ## 插入或替换内容 27 | 28 | 在前言提到的场景中,也有在光标位置处插入内容的需求,比如对选取文字加粗`text => text`等。 29 | 30 | ### textarea 31 | 32 | `textarea`元素可以很容易获取到选择的一段文字的起止位置。如果当前没有选择文字,则两个位置值都为光标右侧字符的索引,从 0 开始。 33 | 34 | ```js 35 | // 开始位置 36 | textarea.selectionStart 37 | // 结束位置 38 | textarea.selectionEnd 39 | ``` 40 | 41 | 对于加粗功能,有了起止位置,就能获取到选择的文字内容,然后对内容进行替换。由于`textarea`不能包含子元素,只有纯文本,所以基于`textarea`实现加粗只能像用 Markdown 标记语法实现。 42 | 43 | ```js 44 | var selectedText = textarea.value.substring(textarea.selectionStart, textarea.selectionEnd) 45 | textarea.setRangeText('**' + selectedText + '**') 46 | ``` 47 | 48 | > `textarea.setRangeText(text: String)` 把选中的文字替换为其他内容。 49 | 50 | ### contenteditable 51 | 52 | 也可能我们会使用`contenteditable`属性把一个元素变为可编辑元素。而上面所用的属性和函数都是普通元素所没有的,所以要换一种姿势实现。 53 | 54 | 还是以加粗功能为例。 55 | 56 | ```js 57 | // 获取文档中选中区域 58 | var range = window.getSelection().getRangeAt(0) 59 | var strongNode = document.createElement('strong') 60 | // 选中区域文本 61 | strongNode.innerHTML = range.toString() 62 | // 删除选中区 63 | range.deleteContents() 64 | // 在光标处插入新节点 65 | range.insertNode(strongNode) 66 | ``` 67 | 68 | 基于`contenteditable`的可编辑元素,其中的内容均为子元素,文本为`textNode`,加粗使用 HTML 元素,插入或替换是对元素的操作。 69 | 70 | 如果想使用操作内容的思路实现会比较麻烦,因为可以获取到的起止位置是基于子元素的。 71 | 72 | ```html 73 |
hello你好world
74 | ``` 75 | 76 | 假如选中的文字是`你好wor`,调用相关 API 的输出如下。 77 | 78 | ```js 79 | // 当前在文档中选择的文本,document 和 window 都有这个函数 80 | // var selection = document.getSelection() 81 | var selection = window.getSelection() 82 | selection.anchorNode // 你好 83 | selection.anchorOffset // 0 84 | selection.focusNode // orld 85 | selection.focusOffset // 2 86 | 87 | // 或者使用 Range 88 | var range = selection.getRangeAt(0) 89 | range.startContainer // 你好 90 | range.startOffset // 0 91 | range.endContainer // orld 92 | range.endOffset // 2 93 | ``` 94 | 95 | 最终可以获取到起止元素以及选中区域在开始元素内容中的字符位置和在结束元素内容中的字符位置。其中的起止元素均为`textNode`类型,通过`parentNode`获取到包裹元素。 96 | 97 | ```js 98 | range.startContainer.parentNode // 你好 99 | range.endContainer.parentNode //
...
100 | ``` 101 | 102 | > 需要注意的是通过`Selection`和`Rang`获取到起止位置是有方向之分的,从左向右选择和从右向左选择得到的值是正好相反的。 103 | 104 | ## 基于光标像素位置创建内容 105 | 106 | 这里就要开始用像素位置,同样分为两种实现来讲。 107 | 108 | ### contenteditable 109 | 110 | 可编辑元素获取光标像素位置就像`textarea`获取光标的字符位置一样简单。 111 | 112 | ```js 113 | var range = window.getSelection().getRangeAt(0) 114 | range.getBoundingClientRect() // { width, height, top, right, bottom, right } 115 | ``` 116 | 117 | 这么具体的尺寸值,实现自动完成真是 So easy! 118 | 119 | ### textarea 120 | 121 | `textarea`其中的内容都是纯文本,在 DOM 中不存在相关的对象,对于像素位置就得另作他想了。 122 | 123 | #### 基于行高和字体大小计算 124 | 125 | ```js 126 | // 1.获取光标结束位置 127 | var end = textarea.selectionEnd 128 | // 2.通过匹配光标之前文本中的换行符计算所在行 129 | var row = textarea.value.substring(0, end).match(/\r\n|\r|\n/).length 130 | // 3.计算 top,行高 * 行数 + 上填充 + 边框宽度 131 | var top = lineHeight * (row + 1) + paddingTop + borderWidth 132 | // 4.获取光标左侧的文本 133 | var leftText = textarea.value.split(/\r\n|\r|\n/)[row] 134 | // 5.影响一段文字所占宽度的因素太多,除字体大小、中英文、符号、字符间距等,还有字体、浏览器、系统等客观因素 135 | // var left = ... 136 | ``` 137 | 138 | 这个方案的思路是没问题的,但是考虑所有问题的成本太高。虽然可以创建测试元素去计算文本宽度,但这个方案本身是从严谨的角度出发的。与其混在一块,直接用取巧的办法更简单。 139 | 140 | > ~~这个方案的潜台词是:明明可以靠脸吃饭,却偏偏要靠才华!~~ 🙄 141 | 142 | #### 镜像元素 143 | 144 | 文本不支持定位?那我创建 DOM 好了。 145 | 146 | ```js 147 | // 光标位置 148 | var end = textarea.selectionEnd 149 | // 光标前的内容 150 | var beforeText = textarea.value.slice(0, end) 151 | // 光标后的内容 152 | var afterText = textarea.value.slice(end) 153 | // 对影响 UI 的特殊元素编码 154 | var escape = function(text) { 155 | return text.replace(/<|>|`|"|&/g, '?').replace(/\r\n|\r|\n/g, '
') 156 | } 157 | // 创建镜像内容,复制样式 158 | var mirror = '
' + escape(beforeText) + '|' + escape(afterText) + '
' 159 | // 添加到 textarea 同级,注意设置定位及 zIndex,使两个元素重合 160 | textarea.insertAdjacentHTML('afterend', mirror) 161 | // 通过镜像元素中的假光标占位元素获取像素位置 162 | var cursor = document.getElementById('cursor') 163 | cursor.getBoundingClientRect() // { width, height, top, right, bottom, right } 164 | ``` 165 | 166 | ## End 167 | 168 | 最后悄悄说一句,以上内容不兼容低版本 IE,但是 IE 毕竟主场运行,有些 API 反而是其他浏览器所没有的。就上面提到的案例来说,低版本 IE 也有对应的 API 可用。真是不想在 IE 上去浪费精力了,索性不提。 169 | -------------------------------------------------------------------------------- /lib/default-theme/Layout.vue: -------------------------------------------------------------------------------- 1 | 22 | 23 | 175 | 176 | 177 | 178 | -------------------------------------------------------------------------------- /lib/default-theme/SearchBox.vue: -------------------------------------------------------------------------------- 1 | 30 | 31 | 134 | 135 | 209 | -------------------------------------------------------------------------------- /lib/default-theme/util.js: -------------------------------------------------------------------------------- 1 | export const hashRE = /#.*$/ 2 | export const extRE = /\.(md|html)$/ 3 | export const endingSlashRE = /\/$/ 4 | export const outboundRE = /^(https?:|mailto:)/ 5 | 6 | export function normalize (path) { 7 | return path 8 | .replace(hashRE, '') 9 | .replace(extRE, '') 10 | } 11 | 12 | export function getHash (path) { 13 | const match = path.match(hashRE) 14 | if (match) { 15 | return match[0] 16 | } 17 | } 18 | 19 | export function isExternal (path) { 20 | return outboundRE.test(path) 21 | } 22 | 23 | export function isMailto (path) { 24 | return /^mailto:/.test(path) 25 | } 26 | 27 | export function ensureExt (path) { 28 | if (isExternal(path)) { 29 | return path 30 | } 31 | const hashMatch = path.match(hashRE) 32 | const hash = hashMatch ? hashMatch[0] : '' 33 | const normalized = normalize(path) 34 | 35 | if (endingSlashRE.test(normalized)) { 36 | return path 37 | } 38 | return normalized + '.html' + hash 39 | } 40 | 41 | export function isActive (route, path) { 42 | const routeHash = route.hash 43 | const linkHash = getHash(path) 44 | if (linkHash && routeHash !== linkHash) { 45 | return false 46 | } 47 | const routePath = normalize(route.path) 48 | const pagePath = normalize(path) 49 | return routePath === pagePath 50 | } 51 | 52 | export function resolvePage (pages, rawPath, base) { 53 | if (base) { 54 | rawPath = resolvePath(rawPath, base) 55 | } 56 | const path = normalize(rawPath) 57 | for (let i = 0; i < pages.length; i++) { 58 | if (normalize(pages[i].path) === path) { 59 | return Object.assign({}, pages[i], { 60 | type: 'page', 61 | path: ensureExt(rawPath) 62 | }) 63 | } 64 | } 65 | console.error(`[vuepress] No matching page found for sidebar item "${rawPath}"`) 66 | return {} 67 | } 68 | 69 | function resolvePath (relative, base, append) { 70 | const firstChar = relative.charAt(0) 71 | if (firstChar === '/') { 72 | return relative 73 | } 74 | 75 | if (firstChar === '?' || firstChar === '#') { 76 | return base + relative 77 | } 78 | 79 | const stack = base.split('/') 80 | 81 | // remove trailing segment if: 82 | // - not appending 83 | // - appending to trailing slash (last segment is empty) 84 | if (!append || !stack[stack.length - 1]) { 85 | stack.pop() 86 | } 87 | 88 | // resolve relative path 89 | const segments = relative.replace(/^\//, '').split('/') 90 | for (let i = 0; i < segments.length; i++) { 91 | const segment = segments[i] 92 | if (segment === '..') { 93 | stack.pop() 94 | } else if (segment !== '.') { 95 | stack.push(segment) 96 | } 97 | } 98 | 99 | // ensure leading slash 100 | if (stack[0] !== '') { 101 | stack.unshift('') 102 | } 103 | 104 | return stack.join('/') 105 | } 106 | 107 | export function resolveSidebarItems (page, route, site, localePath) { 108 | const pageSidebarConfig = page.frontmatter.sidebar 109 | if (pageSidebarConfig === 'auto') { 110 | return resolveHeaders(page) 111 | } 112 | const { pages, themeConfig } = site 113 | 114 | const localeConfig = localePath && themeConfig.locales 115 | ? themeConfig.locales[localePath] || themeConfig 116 | : themeConfig 117 | 118 | const sidebarConfig = localeConfig.sidebar || themeConfig.sidebar 119 | if (!sidebarConfig) { 120 | return [] 121 | } else { 122 | const { base, config } = resolveMatchingConfig(route, sidebarConfig) 123 | return config 124 | ? config.map(item => resolveItem(item, pages, base)) 125 | : [] 126 | } 127 | } 128 | 129 | function resolveHeaders (page) { 130 | const headers = groupHeaders(page.headers || []) 131 | return [{ 132 | type: 'group', 133 | collapsable: false, 134 | title: page.title, 135 | children: headers.map(h => ({ 136 | type: 'auto', 137 | title: h.title, 138 | basePath: page.path, 139 | path: page.path + '#' + h.slug, 140 | children: h.children || [] 141 | })) 142 | }] 143 | } 144 | 145 | export function groupHeaders (headers) { 146 | // group h3s under h2 147 | headers = headers.map(h => Object.assign({}, h)) 148 | let lastH2 149 | headers.forEach(h => { 150 | if (h.level === 2) { 151 | lastH2 = h 152 | } else if (lastH2) { 153 | (lastH2.children || (lastH2.children = [])).push(h) 154 | } 155 | }) 156 | return headers.filter(h => h.level === 2) 157 | } 158 | 159 | export function resolveNavLinkItem (linkItem) { 160 | return Object.assign(linkItem, { 161 | type: linkItem.items && linkItem.items.length ? 'links' : 'link' 162 | }) 163 | } 164 | 165 | export function resolveMatchingConfig (route, config) { 166 | if (Array.isArray(config)) { 167 | return { 168 | base: '/', 169 | config: config 170 | } 171 | } 172 | for (const base in config) { 173 | if (ensureEndingSlash(route.path).indexOf(base) === 0) { 174 | return { 175 | base, 176 | config: config[base] 177 | } 178 | } 179 | } 180 | return {} 181 | } 182 | 183 | function ensureEndingSlash (path) { 184 | return /(\.html|\/)$/.test(path) 185 | ? path 186 | : path + '/' 187 | } 188 | 189 | function resolveItem (item, pages, base, isNested) { 190 | if (typeof item === 'string') { 191 | return resolvePage(pages, item, base) 192 | } else if (Array.isArray(item)) { 193 | return Object.assign(resolvePage(pages, item[0], base), { 194 | title: item[1] 195 | }) 196 | } else { 197 | if (isNested) { 198 | console.error( 199 | '[vuepress] Nested sidebar groups are not supported. ' + 200 | 'Consider using navbar + categories instead.' 201 | ) 202 | } 203 | const children = item.children || [] 204 | return { 205 | type: 'group', 206 | title: item.title, 207 | children: children.map(child => resolveItem(child, pages, base, true)), 208 | collapsable: item.collapsable !== false 209 | } 210 | } 211 | } 212 | -------------------------------------------------------------------------------- /lib/build.js: -------------------------------------------------------------------------------- 1 | module.exports = async function build (sourceDir, cliOptions = {}) { 2 | process.env.NODE_ENV = 'production' 3 | 4 | const fs = require('fs-extra') 5 | const path = require('path') 6 | const chalk = require('chalk') 7 | const webpack = require('webpack') 8 | const readline = require('readline') 9 | const escape = require('escape-html') 10 | 11 | const prepare = require('./prepare') 12 | const createClientConfig = require('./webpack/createClientConfig') 13 | const createServerConfig = require('./webpack/createServerConfig') 14 | const { createBundleRenderer } = require('vue-server-renderer') 15 | const { normalizeHeadTag, applyUserWebpackConfig } = require('./util') 16 | 17 | process.stdout.write('Extracting site metadata...') 18 | const options = await prepare(sourceDir) 19 | if (cliOptions.outDir) { 20 | options.outDir = cliOptions.outDir 21 | } 22 | 23 | const { outDir } = options 24 | await fs.remove(outDir) 25 | 26 | let clientConfig = createClientConfig(options, cliOptions).toConfig() 27 | let serverConfig = createServerConfig(options, cliOptions).toConfig() 28 | 29 | // apply user config... 30 | const userConfig = options.siteConfig.configureWebpack 31 | if (userConfig) { 32 | clientConfig = applyUserWebpackConfig(userConfig, clientConfig, false) 33 | serverConfig = applyUserWebpackConfig(userConfig, serverConfig, true) 34 | } 35 | 36 | // compile! 37 | const stats = await compile([clientConfig, serverConfig]) 38 | 39 | const serverBundle = require(path.resolve(outDir, 'manifest/server.json')) 40 | const clientManifest = require(path.resolve(outDir, 'manifest/client.json')) 41 | 42 | // remove manifests after loading them. 43 | await fs.remove(path.resolve(outDir, 'manifest')) 44 | 45 | // find and remove empty style chunk caused by 46 | // https://github.com/webpack-contrib/mini-css-extract-plugin/issues/85 47 | // TODO remove when it's fixed 48 | await workaroundEmptyStyleChunk() 49 | 50 | // create server renderer using built manifests 51 | const renderer = createBundleRenderer(serverBundle, { 52 | clientManifest, 53 | runInNewContext: false, 54 | inject: false, 55 | template: await fs.readFile(path.resolve(__dirname, 'app/index.ssr.html'), 'utf-8') 56 | }) 57 | 58 | // pre-render head tags from user config 59 | const userHeadTags = (options.siteConfig.head || []).map(renderHeadTag).join('\n ') 60 | 61 | // render pages 62 | console.log('Rendering static HTML...') 63 | for (const page of options.siteData.pages) { 64 | await renderPage(page) 65 | } 66 | 67 | // if the user does not have a custom 404.md, generate the theme's default 68 | if (!options.siteData.pages.some(p => p.path === '/404.html')) { 69 | await renderPage({ path: '/404.html' }) 70 | } 71 | 72 | readline.clearLine(process.stdout, 0) 73 | readline.cursorTo(process.stdout, 0) 74 | 75 | if (options.siteConfig.serviceWorker) { 76 | console.log('Generating service worker...') 77 | const wbb = require('workbox-build') 78 | wbb.generateSW({ 79 | swDest: path.resolve(outDir, 'service-worker.js'), 80 | globDirectory: outDir, 81 | globPatterns: ['**/*.{js,css,html,png,jpg,jpeg,gif,svg,woff,woff2,eot,ttf,otf}'] 82 | }) 83 | } 84 | 85 | // DONE. 86 | const relativeDir = path.relative(process.cwd(), outDir) 87 | console.log(`\n${chalk.green('Success!')} Generated static files in ${chalk.cyan(relativeDir)}.`) 88 | 89 | // --- helpers --- 90 | 91 | function compile (config) { 92 | return new Promise((resolve, reject) => { 93 | webpack(config, (err, stats) => { 94 | if (err) { 95 | return reject(err) 96 | } 97 | if (stats.hasErrors()) { 98 | stats.toJson().errors.forEach(err => { 99 | console.error(err) 100 | }) 101 | reject(new Error(`Failed to compile with errors.`)) 102 | return 103 | } 104 | resolve(stats.toJson({ modules: false })) 105 | }) 106 | }) 107 | } 108 | 109 | function renderHeadTag (tag) { 110 | const { tagName, attributes, innerHTML, closeTag } = normalizeHeadTag(tag) 111 | return `<${tagName}${renderAttrs(attributes)}>${innerHTML}${closeTag ? `` : ``}` 112 | } 113 | 114 | function renderAttrs (attrs = {}) { 115 | const keys = Object.keys(attrs) 116 | if (keys.length) { 117 | return ' ' + keys.map(name => `${name}="${escape(attrs[name])}"`).join(' ') 118 | } else { 119 | return '' 120 | } 121 | } 122 | 123 | async function renderPage (page) { 124 | const pagePath = page.path 125 | readline.clearLine(process.stdout, 0) 126 | readline.cursorTo(process.stdout, 0) 127 | process.stdout.write(`Rendering page: ${pagePath}`) 128 | 129 | const pageMeta = renderPageMeta(page.frontmatter && page.frontmatter.meta) 130 | const context = { 131 | url: pagePath, 132 | userHeadTags, 133 | pageMeta, 134 | title: 'VuePress', 135 | lang: 'en', 136 | description: '' 137 | } 138 | 139 | let html 140 | try { 141 | html = await renderer.renderToString(context) 142 | } catch (e) { 143 | console.error(chalk.red(`Error rendering ${pagePath}:`)) 144 | throw e 145 | } 146 | const filename = pagePath.replace(/\/$/, '/index.html').replace(/^\//, '') 147 | const filePath = path.resolve(outDir, filename) 148 | await fs.ensureDir(path.dirname(filePath)) 149 | await fs.writeFile(filePath, html) 150 | } 151 | 152 | function renderPageMeta (meta) { 153 | if (!meta) return '' 154 | return meta 155 | .map(m => { 156 | let res = ` { 158 | res += ` ${key}="${escape(m[key])}"` 159 | }) 160 | return res + `>` 161 | }) 162 | .join('') 163 | } 164 | 165 | async function workaroundEmptyStyleChunk () { 166 | const styleChunk = stats.children[0].assets.find(a => { 167 | return /styles\.\w{8}\.js$/.test(a.name) 168 | }) 169 | if (!styleChunk) return 170 | const styleChunkPath = path.resolve(outDir, styleChunk.name) 171 | const styleChunkContent = await fs.readFile(styleChunkPath, 'utf-8') 172 | await fs.remove(styleChunkPath) 173 | // prepend it to app.js. 174 | // this is necessary for the webpack runtime to work properly. 175 | const appChunk = stats.children[0].assets.find(a => { 176 | return /app\.\w{8}\.js$/.test(a.name) 177 | }) 178 | const appChunkPath = path.resolve(outDir, appChunk.name) 179 | const appChunkContent = await fs.readFile(appChunkPath, 'utf-8') 180 | await fs.writeFile(appChunkPath, styleChunkContent + appChunkContent) 181 | } 182 | } 183 | -------------------------------------------------------------------------------- /docs/about/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 关于我 · Yusen's Blog · 学习弯道超车的技巧! 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 |
22 | 关于 23 |
27 | 28 | 29 | 30 | -------------------------------------------------------------------------------- /lib/webpack/createBaseConfig.js: -------------------------------------------------------------------------------- 1 | const path = require('path') 2 | 3 | module.exports = function createBaseConfig ( 4 | { siteConfig, sourceDir, outDir, publicPath, themePath, notFoundPath, isAlgoliaSearch, markdown }, 5 | { debug } = {}, 6 | isServer 7 | ) { 8 | const Config = require('webpack-chain') 9 | const { VueLoaderPlugin } = require('vue-loader') 10 | const CSSExtractPlugin = require('mini-css-extract-plugin') 11 | 12 | const isProd = process.env.NODE_ENV === 'production' 13 | const inlineLimit = 10000 14 | 15 | const config = new Config() 16 | 17 | config 18 | .mode(isProd && !debug ? 'production' : 'development') 19 | .output.path(outDir) 20 | .filename(isProd ? 'assets/js/[name].[chunkhash:8].js' : 'assets/js/[name].js') 21 | .publicPath(isProd ? publicPath : '/') 22 | 23 | if (debug) { 24 | config.devtool('source-map') 25 | } else if (!isProd) { 26 | config.devtool('cheap-module-eval-source-map') 27 | } 28 | 29 | config.resolve 30 | .set('symlinks', true) 31 | .alias.set('@theme', themePath) 32 | .set('@notFound', notFoundPath) 33 | .set('@source', sourceDir) 34 | .set('@app', path.resolve(__dirname, '../app')) 35 | .set('@temp', path.resolve(__dirname, '../app/.temp')) 36 | .set('@default-theme', path.resolve(__dirname, '../default-theme')) 37 | .set( 38 | '@AlgoliaSearchBox', 39 | isAlgoliaSearch ? path.resolve(__dirname, '../default-theme/AlgoliaSearchBox.vue') : path.resolve(__dirname, '../noop.js') 40 | ) 41 | .end() 42 | .extensions.merge(['.js', '.jsx', '.vue', '.json']) 43 | .end() 44 | .modules // prioritize our own 45 | .add(path.resolve(__dirname, '../../node_modules')) 46 | .add(path.resolve(__dirname, '../../../')) 47 | .add('node_modules') 48 | 49 | config.resolveLoader 50 | .set('symlinks', true) 51 | .modules // prioritize our own 52 | .add(path.resolve(__dirname, '../../node_modules')) 53 | .add(path.resolve(__dirname, '../../../')) 54 | .add('node_modules') 55 | 56 | config.module.noParse(/^(vue|vue-router|vuex|vuex-router-sync)$/) 57 | 58 | config.module 59 | .rule('vue') 60 | .test(/\.vue$/) 61 | .use('vue-loader') 62 | .loader('vue-loader') 63 | .options({ 64 | compilerOptions: { 65 | preserveWhitespace: false 66 | } 67 | }) 68 | 69 | config.module 70 | .rule('pug') 71 | .test(/\.pug$/) 72 | .use('pug-plain-loader') 73 | .loader('pug-plain-loader') 74 | .end() 75 | 76 | if (!siteConfig.evergreen) { 77 | const libDir = path.join(__dirname, '..') 78 | config.module 79 | .rule('js') 80 | .test(/\.js$/) 81 | .exclude.add(filepath => { 82 | // Always transpile lib directory 83 | if (filepath.startsWith(libDir)) { 84 | return false 85 | } 86 | // Don't transpile node_modules 87 | return /node_modules/.test(filepath) 88 | }) 89 | .end() 90 | .use('buble-loader') 91 | .loader('buble-loader') 92 | .options({ 93 | objectAssign: 'Object.assign' 94 | }) 95 | } 96 | 97 | config.module 98 | .rule('markdown') 99 | .test(/\.md$/) 100 | .use('vue-loader') 101 | .loader('vue-loader') 102 | .options({ 103 | compilerOptions: { 104 | preserveWhitespace: false 105 | } 106 | }) 107 | .end() 108 | .use('markdown-loader') 109 | .loader(require.resolve('./markdownLoader')) 110 | .options({ 111 | sourceDir, 112 | markdown 113 | }) 114 | 115 | config.module 116 | .rule('images') 117 | .test(/\.(png|jpe?g|gif)(\?.*)?$/) 118 | .use('url-loader') 119 | .loader('url-loader') 120 | .options({ 121 | limit: inlineLimit, 122 | name: `assets/img/[name].[hash:8].[ext]` 123 | }) 124 | 125 | // do not base64-inline SVGs. 126 | // https://github.com/facebookincubator/create-react-app/pull/1180 127 | config.module 128 | .rule('svg') 129 | .test(/\.(svg)(\?.*)?$/) 130 | .use('file-loader') 131 | .loader('file-loader') 132 | .options({ 133 | name: `assets/img/[name].[hash:8].[ext]` 134 | }) 135 | 136 | config.module 137 | .rule('media') 138 | .test(/\.(mp4|webm|ogg|mp3|wav|flac|aac)(\?.*)?$/) 139 | .use('url-loader') 140 | .loader('url-loader') 141 | .options({ 142 | limit: inlineLimit, 143 | name: `assets/media/[name].[hash:8].[ext]` 144 | }) 145 | 146 | config.module 147 | .rule('fonts') 148 | .test(/\.(woff2?|eot|ttf|otf)(\?.*)?$/i) 149 | .use('url-loader') 150 | .loader('url-loader') 151 | .options({ 152 | limit: inlineLimit, 153 | name: `assets/fonts/[name].[hash:8].[ext]` 154 | }) 155 | 156 | function createCSSRule (lang, test, loader, options) { 157 | const baseRule = config.module.rule(lang).test(test) 158 | const modulesRule = baseRule.oneOf('modules').resourceQuery(/module/) 159 | const normalRule = baseRule.oneOf('normal') 160 | 161 | applyLoaders(modulesRule, true) 162 | applyLoaders(normalRule, false) 163 | 164 | function applyLoaders (rule, modules) { 165 | if (!isServer) { 166 | if (isProd) { 167 | rule.use('extract-css-loader').loader(CSSExtractPlugin.loader) 168 | } else { 169 | rule.use('vue-style-loader').loader('vue-style-loader') 170 | } 171 | } 172 | 173 | rule 174 | .use('css-loader') 175 | .loader('css-loader') 176 | .options({ 177 | modules, 178 | localIdentName: `[local]_[hash:base64:8]`, 179 | importLoaders: 1 180 | }) 181 | 182 | rule 183 | .use('postcss-loader') 184 | .loader('postcss-loader') 185 | .options( 186 | Object.assign( 187 | { 188 | plugins: [require('autoprefixer')], 189 | sourceMap: !isProd 190 | }, 191 | siteConfig.postcss 192 | ) 193 | ) 194 | 195 | if (loader) { 196 | rule 197 | .use(loader) 198 | .loader(loader) 199 | .options(options) 200 | } 201 | } 202 | } 203 | 204 | createCSSRule('css', /\.css$/) 205 | createCSSRule('scss', /\.scss$/, 'sass-loader', siteConfig.scss) 206 | createCSSRule('sass', /\.sass$/, 'sass-loader', Object.assign({ indentedSyntax: true }, siteConfig.sass)) 207 | createCSSRule('less', /\.less$/, 'less-loader', siteConfig.less) 208 | createCSSRule( 209 | 'stylus', 210 | /\.styl(us)?$/, 211 | 'stylus-loader', 212 | Object.assign( 213 | { 214 | preferPathResolver: 'webpack' 215 | }, 216 | siteConfig.stylus 217 | ) 218 | ) 219 | 220 | config.plugin('vue-loader').use(VueLoaderPlugin) 221 | 222 | if (isProd && !isServer) { 223 | config.plugin('extract-css').use(CSSExtractPlugin, [ 224 | { 225 | filename: 'assets/css/styles.[chunkhash:8].css' 226 | } 227 | ]) 228 | 229 | // ensure all css are extracted together. 230 | // since most of the CSS will be from the theme and very little 231 | // CSS will be from async chunks 232 | config.optimization.splitChunks({ 233 | cacheGroups: { 234 | styles: { 235 | name: 'styles', 236 | // necessary to ensure async chunks are also extracted 237 | test: m => /css-extract/.test(m.type), 238 | chunks: 'all', 239 | enforce: true 240 | } 241 | } 242 | }) 243 | } 244 | 245 | // inject constants 246 | config.plugin('injections').use(require('webpack/lib/DefinePlugin'), [ 247 | { 248 | BASE_URL: JSON.stringify(siteConfig.base || '/'), 249 | GA_ID: siteConfig.ga ? JSON.stringify(siteConfig.ga) : false, 250 | SW_ENABLED: !!siteConfig.serviceWorker 251 | } 252 | ]) 253 | 254 | return config 255 | } 256 | --------------------------------------------------------------------------------