├── .gitattributes ├── .gitignore ├── src ├── event.js ├── css │ ├── global.css │ ├── injected-components.css │ ├── main.css │ ├── prism.css │ └── page-content.css ├── utils │ ├── constants.js │ ├── removeMarkdownExtension.js │ ├── load.js │ ├── alternativeComponents.js │ ├── highlight.js │ ├── parseCodeOptions.js │ ├── index.js │ ├── __test__ │ │ └── index.test.js │ ├── prismLanguages.json │ ├── cssVariables.js │ └── markedRenderer.js ├── plugins │ ├── search │ │ ├── index.js │ │ └── SearchBar.vue │ ├── i18n │ │ ├── index.js │ │ └── LanguageSelector.vue │ ├── versions │ │ ├── index.js │ │ └── VersionsSelector.vue │ ├── evaluateContent │ │ └── index.js │ ├── dark-theme-toggler │ │ ├── index.js │ │ └── DarkThemeToggler.vue │ └── banner-footer │ │ └── index.js ├── components │ ├── SidebarMask.vue │ ├── InjectedComponents.js │ ├── icons │ │ └── ExternalLinkIcon.vue │ ├── Gist.vue │ ├── Badge.vue │ ├── ImageZoom.vue │ ├── UniLink.vue │ ├── EditLink.vue │ ├── Loading.vue │ ├── PageToc.vue │ ├── SidebarToggle.vue │ ├── Root.vue │ ├── Note.vue │ ├── PrevNextLinks.vue │ ├── DocuteSelect.vue │ ├── Sidebar.vue │ ├── Header.vue │ ├── HeaderNav.vue │ └── SidebarItem.vue ├── router.js ├── hooks.js ├── PluginAPI.js ├── index.js ├── views │ └── Home.vue └── store.js ├── .prettierrc ├── .babelrc.js ├── .editorconfig ├── website ├── docs │ ├── zh │ │ ├── credits.md │ │ ├── guide │ │ │ ├── use-with-bundlers.md │ │ │ ├── internationalization.md │ │ │ ├── plugin.md │ │ │ ├── use-vue-in-markdown.md │ │ │ ├── deployment.md │ │ │ ├── offline-support.md │ │ │ ├── customization.md │ │ │ └── markdown-features.md │ │ ├── plugin-api.md │ │ ├── README.md │ │ ├── builtin-components.md │ │ └── options.md │ ├── credits.md │ ├── sw.js │ ├── guide │ │ ├── use-with-bundlers.md │ │ ├── internationalization.md │ │ ├── plugin.md │ │ ├── use-vue-in-markdown.md │ │ ├── deployment.md │ │ ├── offline-support.md │ │ ├── customization.md │ │ └── markdown-features.md │ ├── plugin-api.md │ ├── README.md │ ├── builtin-components.md │ └── options.md ├── components │ └── ColorBox.vue └── index.js ├── postcss.config.js ├── .github └── FUNDING.yml ├── release.config.js ├── now.json ├── circle.yml ├── LICENSE ├── README.md └── package.json /.gitattributes: -------------------------------------------------------------------------------- 1 | * text=auto 2 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | dist 3 | /lib 4 | -------------------------------------------------------------------------------- /src/event.js: -------------------------------------------------------------------------------- 1 | import Vue from 'vue' 2 | 3 | export default new Vue() 4 | -------------------------------------------------------------------------------- /src/css/global.css: -------------------------------------------------------------------------------- 1 | @import "main.css"; 2 | @import "injected-components.css"; 3 | -------------------------------------------------------------------------------- /src/utils/constants.js: -------------------------------------------------------------------------------- 1 | export const INITIAL_STATE_NAME = '__DOCUTE_INITIAL_STATE__' 2 | -------------------------------------------------------------------------------- /.prettierrc: -------------------------------------------------------------------------------- 1 | { 2 | "semi": false, 3 | "singleQuote": true, 4 | "useTabs": false 5 | } 6 | -------------------------------------------------------------------------------- /src/plugins/search/index.js: -------------------------------------------------------------------------------- 1 | import SearchBar from './SearchBar.vue' 2 | 3 | export default { 4 | name: 'search', 5 | extend(api) { 6 | api.registerComponent('header-right:start', SearchBar) 7 | } 8 | } 9 | -------------------------------------------------------------------------------- /.babelrc.js: -------------------------------------------------------------------------------- 1 | const IS_TEST = process.env.NODE_ENV === 'test' 2 | 3 | module.exports = { 4 | presets: [ 5 | ['minimal', { 6 | mode: 'loose' 7 | }], 8 | IS_TEST && 'power-assert' 9 | ].filter(Boolean) 10 | } 11 | -------------------------------------------------------------------------------- /.editorconfig: -------------------------------------------------------------------------------- 1 | root = true 2 | 3 | [*] 4 | indent_style = space 5 | indent_size = 2 6 | end_of_line = lf 7 | charset = utf-8 8 | trim_trailing_whitespace = true 9 | insert_final_newline = true 10 | 11 | [*.md] 12 | trim_trailing_whitespace = false -------------------------------------------------------------------------------- /src/utils/removeMarkdownExtension.js: -------------------------------------------------------------------------------- 1 | const RE = /\.md$/ 2 | 3 | export default input => { 4 | let [path, hash] = input.split('#') 5 | if (RE.test(path)) { 6 | path = path.replace(RE, '') 7 | } 8 | return `${path}${hash ? `#${hash}` : ''}` 9 | } 10 | -------------------------------------------------------------------------------- /website/docs/zh/credits.md: -------------------------------------------------------------------------------- 1 | # 致谢 2 | 3 | Docute 的 UI 灵感来源于下列网站: 4 | 5 | - [Vue.js](https://vuejs.org) 6 | - [ZEIT Docs](https://zeit.co/docs) 7 | 8 | Docute 是一个开源项目,因此也使用了很多优秀的开源项目,比如: 9 | 10 | 13 | -------------------------------------------------------------------------------- /postcss.config.js: -------------------------------------------------------------------------------- 1 | const path = require('path') 2 | 3 | module.exports = { 4 | plugins: [ 5 | require('postcss-import')({ 6 | path: [path.join(__dirname, 'src/css')] 7 | }), 8 | require('postcss-preset-env')({ 9 | stage: 0 10 | }) 11 | ] 12 | } 13 | -------------------------------------------------------------------------------- /src/plugins/i18n/index.js: -------------------------------------------------------------------------------- 1 | import LanguageSelector from './LanguageSelector.vue' 2 | 3 | export default { 4 | name: 'i18n', 5 | extend: api => { 6 | if (api.store.getters.languageOverrides) { 7 | api.registerComponent('sidebar:start', LanguageSelector) 8 | } 9 | } 10 | } 11 | -------------------------------------------------------------------------------- /src/plugins/versions/index.js: -------------------------------------------------------------------------------- 1 | import VersionsSelector from './VersionsSelector.vue' 2 | 3 | export default { 4 | name: 'versions', 5 | extend(api) { 6 | if (api.store.getters.config.versions) { 7 | api.registerComponent('sidebar:start', VersionsSelector) 8 | } 9 | } 10 | } 11 | -------------------------------------------------------------------------------- /src/utils/load.js: -------------------------------------------------------------------------------- 1 | import loadjs from 'loadjs' 2 | 3 | export default (deps, id) => 4 | new Promise(resolve => { 5 | if (loadjs.isDefined(id)) return resolve() 6 | loadjs(deps, id, { 7 | success: resolve, 8 | error(err) { 9 | console.error('Deps not found:', err) 10 | resolve() 11 | } 12 | }) 13 | }) 14 | -------------------------------------------------------------------------------- /src/utils/alternativeComponents.js: -------------------------------------------------------------------------------- 1 | const getComponent = tag => { 2 | return { 3 | functional: true, 4 | render(h, ctx) { 5 | return h(tag, ctx.data, ctx.children) 6 | } 7 | } 8 | } 9 | 10 | export default Vue => { 11 | Vue.component('v-style', getComponent('style')) 12 | Vue.component('v-script', getComponent('script')) 13 | } 14 | -------------------------------------------------------------------------------- /website/docs/credits.md: -------------------------------------------------------------------------------- 1 | # Credits 2 | 3 | Docute's UI is inspired by these websites: 4 | 5 | - [Vue.js](https://vuejs.org) 6 | - [ZEIT Docs](https://zeit.co/docs) 7 | 8 | Docute is a free and open-source project which is built upon many other open-source projects, such as: 9 | 10 | 13 | -------------------------------------------------------------------------------- /src/utils/highlight.js: -------------------------------------------------------------------------------- 1 | import Prism from 'prismjs' 2 | 3 | export default function highlight(str, lang) { 4 | if (!lang) return str 5 | 6 | let resolvedLang = lang && Prism.languages[lang] 7 | if (!resolvedLang) { 8 | lang = 'markup' 9 | resolvedLang = Prism.languages.markup 10 | } 11 | 12 | return Prism.highlight(str, resolvedLang, lang) 13 | } 14 | -------------------------------------------------------------------------------- /website/components/ColorBox.vue: -------------------------------------------------------------------------------- 1 | 4 | 5 | 10 | 11 | 12 | 20 | -------------------------------------------------------------------------------- /website/docs/sw.js: -------------------------------------------------------------------------------- 1 | self.addEventListener('install', e => { 2 | self.skipWaiting() 3 | }) 4 | 5 | self.addEventListener('activate', e => { 6 | self.registration 7 | .unregister() 8 | .then(() => { 9 | return self.clients.matchAll() 10 | }) 11 | .then(clients => { 12 | clients.forEach(client => client.navigate(client.url)) 13 | }) 14 | }) 15 | -------------------------------------------------------------------------------- /src/components/SidebarMask.vue: -------------------------------------------------------------------------------- 1 | 8 | 9 | 19 | -------------------------------------------------------------------------------- /.github/FUNDING.yml: -------------------------------------------------------------------------------- 1 | # These are supported funding model platforms 2 | 3 | github: egoist # Replace with up to 4 GitHub Sponsors-enabled usernames e.g., [user1, user2] 4 | open_collective: # Replace with a single Open Collective username 5 | ko_fi: support_egoist 6 | tidelift: # Replace with a single Tidelift platform-name/package-name e.g., npm/babel 7 | custom: # Replace with a single custom sponsorship URL 8 | -------------------------------------------------------------------------------- /release.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | branch: 'master', 3 | plugins: [ 4 | [ 5 | '@semantic-release/commit-analyzer', 6 | { 7 | preset: 'angular', 8 | releaseRules: [{type: 'feat', scope: 'ui', release: 'patch'}] 9 | } 10 | ], 11 | '@semantic-release/release-notes-generator', 12 | '@semantic-release/npm', 13 | '@semantic-release/github' 14 | ] 15 | } 16 | -------------------------------------------------------------------------------- /src/utils/parseCodeOptions.js: -------------------------------------------------------------------------------- 1 | export default opts => { 2 | if (opts) { 3 | try { 4 | // eslint-disable-next-line no-new-func 5 | const fn = new Function(`return ${opts}`) 6 | opts = fn() 7 | } catch (error) { 8 | console.error( 9 | `You're using invalid options for code fences, it must be JSON or JS object!\n${error.message}` 10 | ) 11 | } 12 | } 13 | return opts || {} 14 | } 15 | -------------------------------------------------------------------------------- /src/plugins/evaluateContent/index.js: -------------------------------------------------------------------------------- 1 | export default { 2 | name: 'hoistTags', 3 | extend(api) { 4 | api.extendMarkedRenderer(renderer => { 5 | const hoistedTagsRe = /^<(script|style)(?=(\s|>|$))/i 6 | renderer.html = html => { 7 | html = html.trim() 8 | if (hoistedTagsRe.test(html)) { 9 | return html 10 | .replace(/^<(script|style)/, '$/, '') 12 | } 13 | return html 14 | } 15 | }) 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /src/plugins/dark-theme-toggler/index.js: -------------------------------------------------------------------------------- 1 | import DarkThemeToggler from './DarkThemeToggler.vue' 2 | 3 | export default { 4 | name: 'dark-theme-toggler', 5 | extend: api => { 6 | const position = api.store.getters.config.darkThemeToggler 7 | if (position === true) { 8 | api.registerComponent('sidebar:post-end', DarkThemeToggler) 9 | } else if (position === 'sidebar') { 10 | api.registerComponent('header-right:start', DarkThemeToggler) 11 | api.registerComponent('mobile-sidebar:start', DarkThemeToggler) 12 | } 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /src/router.js: -------------------------------------------------------------------------------- 1 | import Vue from 'vue' 2 | import Router from 'vue-router' 3 | import RouterPrefetch from 'vue-router-prefetch' 4 | import Home from './views/Home.vue' 5 | 6 | Vue.use(Router) 7 | Vue.use(RouterPrefetch) 8 | 9 | export default routerConfig => 10 | new Router({ 11 | scrollBehavior(to, from, savedPosition) { 12 | if (savedPosition) { 13 | return savedPosition 14 | } 15 | return {x: 0, y: 0} 16 | }, 17 | ...routerConfig, 18 | routes: [ 19 | { 20 | path: '*', 21 | component: Home 22 | } 23 | ] 24 | }) 25 | -------------------------------------------------------------------------------- /src/plugins/banner-footer/index.js: -------------------------------------------------------------------------------- 1 | const getComponent = (str, className) => 2 | typeof str === 'string' 3 | ? {template: `
${str}
`} 4 | : str 5 | 6 | export default { 7 | name: 'banner-footer', 8 | extend(api) { 9 | const {banner, footer} = api.store.getters.config 10 | if (banner) { 11 | api.registerComponent( 12 | 'content:start', 13 | getComponent(banner, 'docute-banner') 14 | ) 15 | } 16 | if (footer) { 17 | api.registerComponent( 18 | 'content:end', 19 | getComponent(footer, 'docute-footer') 20 | ) 21 | } 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /src/components/InjectedComponents.js: -------------------------------------------------------------------------------- 1 | export default { 2 | name: 'InjectedComponents', 3 | 4 | functional: true, 5 | 6 | props: { 7 | position: { 8 | type: String, 9 | required: true 10 | } 11 | }, 12 | 13 | render(h, {props, parent}) { 14 | const components = parent.$pluginApi.getComponents(props.position) 15 | 16 | if (components.length === 0) return 17 | 18 | return h( 19 | 'div', 20 | { 21 | class: 'InjectedComponents', 22 | attrs: { 23 | 'data-position': props.position 24 | } 25 | }, 26 | components.map(({component, props}) => h(component, {props})) 27 | ) 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /now.json: -------------------------------------------------------------------------------- 1 | { 2 | "version": 2, 3 | "builds": [ 4 | { 5 | "src": "package.json", 6 | "use": "@now/static-build", 7 | "config": { 8 | "distDir": "website/dist" 9 | } 10 | } 11 | ], 12 | "routes": [ 13 | { 14 | "src": "^/sw.js", 15 | "headers": { "cache-control": "s-maxage=0" }, 16 | "dest": "/sw.js" 17 | }, 18 | { 19 | "src": "^/(.+)\\.(.+)", 20 | "headers": { "cache-control": "s-maxage=0, public, max-age=0, must-revalidate" }, 21 | "dest": "/$1.$2" 22 | }, 23 | { 24 | "src": "^/(.*)", 25 | "dest": "/index.html" 26 | } 27 | ], 28 | "github": { 29 | "silent": true 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /circle.yml: -------------------------------------------------------------------------------- 1 | version: 2 2 | jobs: 3 | build: 4 | docker: 5 | - image: circleci/node:latest-browsers 6 | branches: 7 | ignore: 8 | - gh-pages # list of branches to ignore 9 | - /release\/.*/ # or ignore regexes 10 | steps: 11 | - checkout 12 | - restore_cache: 13 | key: dependency-cache-{{ checksum "package-lock.json" }} 14 | - run: 15 | name: install dependences 16 | command: npm ci 17 | - save_cache: 18 | key: dependency-cache-{{ checksum "package-lock.json" }} 19 | paths: 20 | - ./node_modules 21 | - run: 22 | name: test 23 | command: npm test 24 | - run: 25 | name: Release 26 | command: npx semantic-release 27 | -------------------------------------------------------------------------------- /src/components/icons/ExternalLinkIcon.vue: -------------------------------------------------------------------------------- 1 | 22 | 23 | 28 | -------------------------------------------------------------------------------- /website/docs/guide/use-with-bundlers.md: -------------------------------------------------------------------------------- 1 | # Use With Bundlers 2 | 3 | You don't have to use a bundler in order to use Docute, however if you want, you can! 4 | 5 | First you need to install Docute as a dependency in your project: 6 | 7 | ```bash 8 | npm install docute 9 | ``` 10 | 11 | Then see below for the usage with your bundler of choice. 12 | 13 | ## Webpack 14 | 15 | In your entry file: 16 | 17 | ```js 18 | import Docute from 'docute' 19 | 20 | new Docute({ 21 | target: '#app', 22 | // Other options 23 | }) 24 | ``` 25 | 26 | You may need to use [html-webpack-plugin](https://github.com/jantimon/html-webpack-plugin) to generate an HTML file too. 27 | 28 | If you're using [Vue CLI](https://cli.vuejs.org) or [Poi](https://poi.js.org), congratulations, there's no more build configs needed. 29 | 30 | ## Parcel 31 | 32 | [TODO] [PR WELCOME] 33 | -------------------------------------------------------------------------------- /website/docs/zh/guide/use-with-bundlers.md: -------------------------------------------------------------------------------- 1 | # Use With Bundlers 2 | 3 | You don't have to use a bundler in order to use Docute, however if you want, you can! 4 | 5 | First you need to install Docute as a dependency in your project: 6 | 7 | ```bash 8 | npm install docute 9 | ``` 10 | 11 | Then see below for the usage with your bundler of choice. 12 | 13 | ## Webpack 14 | 15 | In your entry file: 16 | 17 | ```js 18 | import Docute from 'docute' 19 | 20 | new Docute({ 21 | target: '#app', 22 | // Other options 23 | }) 24 | ``` 25 | 26 | You may need to use [html-webpack-plugin](https://github.com/jantimon/html-webpack-plugin) to generate an HTML file too. 27 | 28 | If you're using [Vue CLI](https://cli.vuejs.org) or [Poi](https://poi.js.org), congratulations, there's no more build configs needed. 29 | 30 | ## Parcel 31 | 32 | [TODO] [PR WELCOME] 33 | -------------------------------------------------------------------------------- /src/hooks.js: -------------------------------------------------------------------------------- 1 | class Hooks { 2 | constructor() { 3 | this.hooks = {} 4 | } 5 | 6 | add(name, fn) { 7 | this.hooks[name] = this.hooks[name] || [] 8 | this.hooks[name].push(fn) 9 | return this 10 | } 11 | 12 | invoke(name, ...args) { 13 | const hooks = this.hooks[name] || [] 14 | for (const fn of hooks) { 15 | fn(...args) 16 | } 17 | return this 18 | } 19 | 20 | process(name, arg) { 21 | const hooks = this.hooks[name] || [] 22 | for (const fn of hooks) { 23 | arg = fn(arg) || arg 24 | } 25 | return arg 26 | } 27 | 28 | async processPromise(name, arg) { 29 | const hooks = this.hooks[name] || [] 30 | for (const fn of hooks) { 31 | // eslint-disable-next-line no-await-in-loop 32 | arg = (await fn(arg)) || arg 33 | } 34 | return arg 35 | } 36 | } 37 | 38 | export default new Hooks() 39 | -------------------------------------------------------------------------------- /src/components/Gist.vue: -------------------------------------------------------------------------------- 1 | 5 | 6 | 37 | 38 | 43 | -------------------------------------------------------------------------------- /website/docs/zh/guide/internationalization.md: -------------------------------------------------------------------------------- 1 | # 国际化 2 | 3 | 由于 Docute 使用基于 URL 的 API实现,因此添加多语言支持非常简单: 4 | 5 | ``` 6 | docs 7 | ├─ README.md 8 | ├─ foo.md 9 | ├─ nested 10 | │ └─ README.md 11 | └─ zh 12 | ├─ README.md 13 | ├─ foo.md 14 | └─ nested 15 | └─ README.md 16 | ``` 17 | 18 | 使用上述文件夹结构,用户可以通过 URL `/zh/` 访问文档的*中文*版本。 19 | 20 | 然后,可以使用 `overrides` 选项来本地化 UI 中使用的文本: 21 | 22 | ```js 23 | new Docute({ 24 | sidebar: [ 25 | { 26 | children: [ 27 | { title: 'Guide', link: '/guide' } 28 | ] 29 | } 30 | ], 31 | overrides: { 32 | '/': { 33 | language: 'English' // Used by the language dropdown menu in the sidebar 34 | }, 35 | '/zh/': { 36 | language: 'Chinese', 37 | // Override the default sidebar 38 | sidebar: [ 39 | { 40 | children: [ 41 | { title: '指南', link: '/zh/guide' } 42 | ] 43 | } 44 | ] 45 | } 46 | } 47 | }) 48 | ``` 49 | -------------------------------------------------------------------------------- /website/docs/zh/guide/plugin.md: -------------------------------------------------------------------------------- 1 | # 插件 2 | 3 | 插件本质上是一个纯对象(pure object): 4 | 5 | ```js 6 | const showAuthor = { 7 | // 插件名称 8 | name: 'showAuthor', 9 | // 扩展核心功能 10 | extend(api) { 11 | api.processMarkdown(text => { 12 | return text.replace(/{author}/g, '> Written by EGOIST') 13 | }) 14 | } 15 | } 16 | 17 | new Docute({ 18 | // ... 19 | plugins: [ 20 | showAuthor 21 | ] 22 | }) 23 | ``` 24 | 25 | 示例: 26 | 27 | ```markdown 28 | # Page 标题 29 | 30 | {author} 31 | ``` 32 | 33 | 34 | 35 | --- 36 | 37 | 要接收插件中的选项,可以使用工厂函数: 38 | 39 | ```js 40 | const myPlugin = opts => { 41 | return { 42 | name: 'my-plugin', 43 | extend(api) { 44 | // 使用 `opts` 和 `api` 做点什么 45 | } 46 | } 47 | } 48 | 49 | new Docute({ 50 | plugins: [ 51 | myPlugin({ foo: true }) 52 | ] 53 | }) 54 | ``` 55 | 56 | --- 57 | 58 | 欲了解更多如何开发插件的信息,请查阅[插件 API](/zh/plugin-api)。 59 | -------------------------------------------------------------------------------- /src/components/Badge.vue: -------------------------------------------------------------------------------- 1 | 9 | 10 | 23 | 24 | 54 | -------------------------------------------------------------------------------- /src/css/injected-components.css: -------------------------------------------------------------------------------- 1 | .InjectedComponents { 2 | &[data-position="sidebar:start"], 3 | &[data-position="mobile-sidebar:start"] { 4 | margin-bottom: 25px; 5 | 6 | &>*:first-child { 7 | margin-top: 0; 8 | } 9 | } 10 | 11 | &[data-position="sidebar:end"], 12 | &[data-position="sidebar:post-end"] { 13 | margin-top: 25px; 14 | 15 | &>*:first-child { 16 | margin-top: 0; 17 | } 18 | } 19 | 20 | &[data-position="content:start"] { 21 | margin-bottom: 35px; 22 | 23 | &>*:first-child { 24 | margin-top: 0; 25 | } 26 | 27 | &>*:last-child { 28 | margin-bottom: 0; 29 | } 30 | } 31 | 32 | &[data-position="content:end"] { 33 | margin-top: 30px; 34 | } 35 | 36 | &[data-position="header-right:start"] + .header-nav { 37 | margin-left: 30px; 38 | } 39 | } 40 | 41 | @media (min-width: 768px) { 42 | .InjectedComponents { 43 | &[data-position="mobile-sidebar:start"], 44 | &[data-position="mobile-sidebar:end"] { 45 | display: none; 46 | } 47 | } 48 | } 49 | -------------------------------------------------------------------------------- /website/docs/guide/internationalization.md: -------------------------------------------------------------------------------- 1 | # Internationalization 2 | 3 | As Docute uses a URL-based API, adding multi-language support can be pretty easy: 4 | 5 | ``` 6 | docs 7 | ├─ README.md 8 | ├─ foo.md 9 | ├─ nested 10 | │ └─ README.md 11 | └─ zh 12 | ├─ README.md 13 | ├─ foo.md 14 | └─ nested 15 | └─ README.md 16 | ``` 17 | 18 | With above folder structure, users can visit the *Chinese* version of your docs via URL `/zh/`. 19 | 20 | Then you can use the `overrides` option to localize the text used in UI: 21 | 22 | ```js 23 | new Docute({ 24 | sidebar: [ 25 | { 26 | children: [ 27 | { title: 'Guide', link: '/guide' } 28 | ] 29 | } 30 | ], 31 | overrides: { 32 | '/': { 33 | language: 'English' // Used by the language dropdown menu in the sidebar 34 | }, 35 | '/zh/': { 36 | language: 'Chinese', 37 | // Override the default sidebar 38 | sidebar: [ 39 | { 40 | children: [ 41 | { title: '指南', link: '/zh/guide' } 42 | ] 43 | } 44 | ] 45 | } 46 | } 47 | }) 48 | ``` 49 | -------------------------------------------------------------------------------- /src/utils/index.js: -------------------------------------------------------------------------------- 1 | export const isExternalLink = link => { 2 | return /^https?:\/\//.test(link) 3 | } 4 | 5 | export const slugify = str => { 6 | const RE = /[\u2000-\u206F\u2E00-\u2E7F\\'!"#$%&()*+,./:;<=>?@[\]^`{|}~]/g 7 | const REPLACEMENT = '-' 8 | const WHITESPACE = /\s/g 9 | 10 | return str 11 | .trim() 12 | .replace(RE, '') 13 | .replace(WHITESPACE, REPLACEMENT) 14 | .toLowerCase() 15 | } 16 | 17 | export const getFileUrl = (sourcePath, path) => { 18 | sourcePath = sourcePath || '.' 19 | 20 | // Remove trailing slash in `sourcePath` 21 | // Since `path` always starts with slash 22 | sourcePath = sourcePath.replace(/\/$/, '') 23 | 24 | const result = sourcePath + path 25 | 26 | return result.replace(/^\.\//, '') 27 | } 28 | 29 | export const getFilenameByPath = path => { 30 | // Ensure path always starts with slash 31 | path = path.replace(/^\/?/, '/') 32 | 33 | // Add .md suffix 34 | if (!/\.md$/.test(path)) { 35 | path = /\/$/.test(path) ? `${path}README.md` : `${path}.md` 36 | } 37 | 38 | return path 39 | } 40 | 41 | export const inBrowser = typeof window !== 'undefined' 42 | -------------------------------------------------------------------------------- /src/utils/__test__/index.test.js: -------------------------------------------------------------------------------- 1 | import assert from 'assert' 2 | import {getFilenameByPath, getFileUrl} from '..' 3 | 4 | describe('utils/index', () => { 5 | it('getFilenameByPath', () => { 6 | assert(getFilenameByPath('/') === '/README.md') 7 | assert(getFilenameByPath('/foo') === '/foo.md') 8 | assert(getFilenameByPath('foo') === '/foo.md') 9 | assert(getFilenameByPath('/foo/bar/') === '/foo/bar/README.md') 10 | }) 11 | 12 | it('getFileUrl', () => { 13 | assert(getFileUrl(null, getFilenameByPath('/')) === 'README.md') 14 | assert(getFileUrl('/', getFilenameByPath('/')) === '/README.md') 15 | assert(getFileUrl('./', getFilenameByPath('/')) === 'README.md') 16 | assert(getFileUrl('./docs/', getFilenameByPath('/')) === 'docs/README.md') 17 | assert(getFileUrl('/docs/', getFilenameByPath('/')) === '/docs/README.md') 18 | assert(getFileUrl('/docs/', getFilenameByPath('/foo')) === '/docs/foo.md') 19 | assert( 20 | getFileUrl('/docs/', getFilenameByPath('/foo.md')) === '/docs/foo.md' 21 | ) 22 | assert( 23 | getFileUrl('/docs/', getFilenameByPath('/foo/')) === '/docs/foo/README.md' 24 | ) 25 | }) 26 | }) 27 | -------------------------------------------------------------------------------- /src/components/ImageZoom.vue: -------------------------------------------------------------------------------- 1 | 6 | 7 | 47 | 48 | 60 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) egoist <0x142857@gmail.com> (https://github.com/egoist) 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 13 | all 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 21 | THE SOFTWARE. 22 | -------------------------------------------------------------------------------- /src/css/main.css: -------------------------------------------------------------------------------- 1 | body { 2 | margin: 0; 3 | color: var(--text-color); 4 | background: var(--page-background); 5 | text-rendering: optimizeLegibility; 6 | -webkit-font-smoothing: antialiased; 7 | font: 16px/1.7 -apple-system, BlinkMacSystemFont, "Segoe UI", "Roboto", "Oxygen", "Ubuntu", "Cantarell", "Fira Sans", "Droid Sans", "Helvetica Neue", sans-serif; 8 | } 9 | 10 | * { 11 | box-sizing: border-box; 12 | } 13 | 14 | a { 15 | color: var(--link-color); 16 | text-decoration: none; 17 | } 18 | 19 | .Content a { 20 | &:hover { 21 | text-decoration: underline; 22 | } 23 | } 24 | 25 | .external-link-icon { 26 | color: #aaa; 27 | display: inline-block; 28 | } 29 | 30 | .medium-zoom-overlay, .medium-zoom-image--opened { 31 | z-index: 99; 32 | } 33 | 34 | .Wrap { 35 | max-width: 1180px; 36 | } 37 | 38 | .layout-wide .Wrap { 39 | max-width: 100%; 40 | } 41 | 42 | .layout-narrow .Wrap { 43 | margin: 0 auto; 44 | } 45 | 46 | .docute-banner { 47 | margin-bottom: 10px; 48 | 49 | &>*:first-child { 50 | margin-top: 0; 51 | } 52 | 53 | &>*:last-child { 54 | margin-bottom: 0; 55 | } 56 | } 57 | 58 | .docute-footer { 59 | padding-top: 60px; 60 | } 61 | -------------------------------------------------------------------------------- /src/components/UniLink.vue: -------------------------------------------------------------------------------- 1 | 46 | 47 | 52 | -------------------------------------------------------------------------------- /src/components/EditLink.vue: -------------------------------------------------------------------------------- 1 | 24 | 25 | 38 | 39 | 55 | -------------------------------------------------------------------------------- /website/docs/guide/plugin.md: -------------------------------------------------------------------------------- 1 | # Plugin 2 | 3 | A plugin is essentially a pure object: 4 | 5 | ```js 6 | const showAuthor = { 7 | // Plugin name 8 | name: 'showAuthor', 9 | // Extend core features 10 | extend(api) { 11 | api.processMarkdown(text => { 12 | return text.replace(/{author}/g, '> Written by EGOIST') 13 | }) 14 | } 15 | } 16 | 17 | new Docute({ 18 | // ... 19 | plugins: [ 20 | showAuthor 21 | ] 22 | }) 23 | ``` 24 | 25 | Example: 26 | 27 | ```markdown 28 | # Page Title 29 | 30 | {author} 31 | ``` 32 | 33 | 34 | 35 | --- 36 | 37 | To accept options in your plugin, you can use a factory function: 38 | 39 | ```js 40 | const myPlugin = opts => { 41 | return { 42 | name: 'my-plugin', 43 | extend(api) { 44 | // do something with `opts` and `api` 45 | } 46 | } 47 | } 48 | 49 | new Docute({ 50 | plugins: [ 51 | myPlugin({ foo: true }) 52 | ] 53 | }) 54 | ``` 55 | 56 | --- 57 | 58 | For more information on how to develop a plugin, please check out [Plugin API](/plugin-api). 59 | 60 | Check out [https://github.com/egoist/docute-plugins](https://github.com/egoist/docute-plugins) for a list of Docute plugins by the maintainers and users. 61 | -------------------------------------------------------------------------------- /src/components/Loading.vue: -------------------------------------------------------------------------------- 1 | 7 | 8 | 13 | 14 | 65 | -------------------------------------------------------------------------------- /src/plugins/i18n/LanguageSelector.vue: -------------------------------------------------------------------------------- 1 | 19 | 20 | 43 | 44 | 50 | -------------------------------------------------------------------------------- /src/components/PageToc.vue: -------------------------------------------------------------------------------- 1 | 22 | 23 | 33 | 34 | 60 | -------------------------------------------------------------------------------- /src/plugins/versions/VersionsSelector.vue: -------------------------------------------------------------------------------- 1 | 20 | 21 | 49 | 50 | 56 | -------------------------------------------------------------------------------- /src/components/SidebarToggle.vue: -------------------------------------------------------------------------------- 1 | 11 | 12 | 31 | 32 | 53 | -------------------------------------------------------------------------------- /src/components/Root.vue: -------------------------------------------------------------------------------- 1 | 58 | 59 | 60 | -------------------------------------------------------------------------------- /website/docs/zh/guide/use-vue-in-markdown.md: -------------------------------------------------------------------------------- 1 | # 在 Markdown 中使用 Vue 2 | 3 | 在编写 Markdown 文档时可以充分利用 Vue 和 JavaScript 的强大功能! 4 | 5 | ## 插值(Interpolation) 6 | 7 | 每个 Markdown 文件首先会编译为 HTML,然后渲染为 Vue 组件这意味着你可以在文本中使用 Vue 式的插值: 8 | 9 | __输入__: 10 | 11 | ```markdown 12 | {{ 1 + 1 }} 13 | ``` 14 | 15 | __输出__: 16 | 17 | ``` 18 | 2 19 | ``` 20 | 21 | ## Escaping 22 | 23 | 如果要在文本中禁用 Vue 式插值,可以将其包装在代码块或内联代码中,如下所示: 24 | 25 | __输入__: 26 | 27 | ````markdown 28 | ```js 29 | const foo = `{{ 安全,这不会被插值!}}` 30 | ``` 31 | 32 | `{{ bar }}` 也是安全的! 33 | ```` 34 | 35 | __输出__: 36 | 37 | ```js 38 | const foo = `{{ 安全,这不会被插值!}}` 39 | ``` 40 | 41 | `{{ bar }}` 也是安全的! 42 | 43 | ## 使用组件 44 | 45 | Docute 在 `window` 对象上暴露了 `Vue` 的构造函数,因此你可以使用它来注册全局组件,以便于在 Markdown 文档中使用: 46 | 47 | ```js {highlight:['6-13']} 48 | Vue.component('ReverseText', { 49 | props: ['text'], 50 | template: ` 51 |
52 | {{ reversedText }} 53 | 54 | .reverse-text { 55 | border: 1px solid var(--border-color); 56 | padding: 20px; 57 | font-weight: bold; 58 | border-radius: 4px; 59 | } 60 | 61 |
62 | `, 63 | computed: { 64 | reversedText() { 65 | return this.text.split('').reverse().join('') 66 | } 67 | } 68 | }) 69 | ``` 70 | 71 | 你可能会注意到高亮的部分,由于你不能直接在 Vue template 里使用 `style` 元素,于是我们提供了 `v-style` 组件来作为替代。类似地,我们也提供了 `v-script` 组件。 72 | 73 | __输入__: 74 | 75 | ```markdown 76 | 77 | ``` 78 | 79 | __输出__: 80 | 81 | 82 | 83 | -------------------------------------------------------------------------------- /website/docs/zh/guide/deployment.md: -------------------------------------------------------------------------------- 1 | # 部署 2 | 3 | 请记住,它只是一个在任何地方都可以提供的静态 HTML 文件。 4 | 5 | ## ZEIT Now Recommended 6 | 7 | [ZEIT Now](https://zeit.co/now) is a platform for Global Serverless Deployments, it's also perfectly suitable for deploying a static website with or without build process. 8 | 9 | Assuming you have your docs in `./docs` folder, to deploy it you can simply populate a `now.json` in your project: 10 | 11 | ```json 12 | { 13 | "version": 2, 14 | "builds": [ 15 | { 16 | "src": "docs/**", 17 | "use": "@now/static" 18 | } 19 | ] 20 | } 21 | ``` 22 | 23 | Then [install Now](https://zeit.co/docs/v2/getting-started/installation/) on your machine. 24 | 25 | After that, you can run the command `now` in your project and you're all set. 26 | 27 | Make sure to check out Now's [GitHub Integration](https://zeit.co/docs/v2/integrations/now-for-github/) if you want automatic deployments on every push and pull request. 28 | 29 | ## Netlify Recommended 30 | 31 | 1. 登录你的 [Netlify](https://www.netlify.com/) 账号。 32 | 2. 在 [dashboard](https://app.netlify.com/) 页,点击 __New site from Git__. 33 | 3. 选择一个存储文档的仓库,将 __Build Command__ 区域留空, blank,用 `index.html` 的目录填写 __Publish directory__ 区域,例如,`docs/index.html`,应填为 `docs`。 34 | 35 | ## GitHub Pages 36 | 37 | 使用 Github Pages 最简单的方式是在 master 分支上的 `./docs` 文件夹中加入所有文件,然后将此文件夹用于 Github Pages: 38 | 39 | 40 | 41 | 但是你仍然可以使用 `gh-pages` 分支或者 `master` 分支来为你的文档提供服务,这一切都取决于你的需求。 42 | -------------------------------------------------------------------------------- /src/PluginAPI.js: -------------------------------------------------------------------------------- 1 | import Vue from 'vue' 2 | import InjectedComponents from './components/InjectedComponents' 3 | import hooks from './hooks' 4 | 5 | export default class PluginAPI { 6 | constructor({plugins, store, router}) { 7 | this.plugins = plugins 8 | this.store = store 9 | this.router = router 10 | this.components = {} 11 | this.hooks = hooks 12 | this.search = {} 13 | 14 | Vue.component(InjectedComponents.name, InjectedComponents) 15 | } 16 | 17 | hasPlugin(name) { 18 | return this.plugins.filter(plugin => plugin.name === name).length > 0 19 | } 20 | 21 | registerComponent(position, component, props) { 22 | this.components[position] = this.components[position] || [] 23 | this.components[position].push({component, props}) 24 | return this 25 | } 26 | 27 | getComponents(position) { 28 | return this.components[position] || [] 29 | } 30 | 31 | processMarkdown(fn) { 32 | this.hooks.add('processMarkdown', fn) 33 | return this 34 | } 35 | 36 | processHTML(fn) { 37 | this.hooks.add('processHTML', fn) 38 | return this 39 | } 40 | 41 | extendMarkedRenderer(fn) { 42 | this.hooks.add('extendMarkedRenderer', fn) 43 | return this 44 | } 45 | 46 | onContentUpdated(fn) { 47 | this.hooks.add('onContentUpdated', fn) 48 | return this 49 | } 50 | 51 | extendMarkdownComponent(fn) { 52 | this.hooks.add('extendMarkdownComponent', fn) 53 | return this 54 | } 55 | 56 | enableSearch(search = {}) { 57 | this.search = search 58 | this.search.enabled = true 59 | return this 60 | } 61 | } 62 | -------------------------------------------------------------------------------- /src/css/prism.css: -------------------------------------------------------------------------------- 1 | 2 | /* https://github.com/SaraVieira/prism-theme-night-owl */ 3 | 4 | .token.comment, 5 | .token.prolog, 6 | .token.cdata { 7 | color: rgb(99, 119, 119); 8 | font-style: italic; 9 | } 10 | 11 | .token.punctuation { 12 | color: rgb(199, 146, 234); 13 | } 14 | 15 | .namespace { 16 | color: rgb(178, 204, 214); 17 | } 18 | 19 | .token.deleted { 20 | color: rgba(239, 83, 80, 0.56); 21 | font-style: italic; 22 | } 23 | 24 | .token.symbol, 25 | .token.property { 26 | color: rgb(128, 203, 196); 27 | } 28 | 29 | .token.tag, 30 | .token.operator, 31 | .token.keyword { 32 | color: rgb(127, 219, 202); 33 | } 34 | 35 | .token.boolean { 36 | color: rgb(255, 88, 116); 37 | } 38 | 39 | .token.number { 40 | color: rgb(247, 140, 108); 41 | } 42 | 43 | .token.constant, 44 | .token.function, 45 | .token.builtin, 46 | .token.char { 47 | color: rgb(130, 170, 255); 48 | } 49 | 50 | .token.selector, 51 | .token.doctype { 52 | color: rgb(199, 146, 234); 53 | font-style: italic; 54 | } 55 | 56 | .token.attr-name, 57 | .token.inserted { 58 | color: rgb(173, 219, 103); 59 | font-style: italic; 60 | } 61 | 62 | .token.string, 63 | .token.url, 64 | .token.entity, 65 | .language-css .token.string, 66 | .style .token.string { 67 | color: rgb(173, 219, 103); 68 | } 69 | 70 | .token.class-name, 71 | .token.atrule, 72 | .token.attr-value { 73 | color: rgb(255, 203, 139); 74 | } 75 | 76 | .token.regex, 77 | .token.important, 78 | .token.variable { 79 | color: rgb(214, 222, 235); 80 | } 81 | 82 | .token.important, 83 | .token.bold { 84 | font-weight: bold; 85 | } 86 | 87 | .token.italic { 88 | font-style: italic; 89 | } 90 | -------------------------------------------------------------------------------- /src/components/Note.vue: -------------------------------------------------------------------------------- 1 | 9 | 10 | 25 | 26 | 81 | -------------------------------------------------------------------------------- /src/components/PrevNextLinks.vue: -------------------------------------------------------------------------------- 1 | 16 | 17 | 56 | 57 | 77 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | ## ATTENTION: this project is no longer actively maintained (I still push some code once in a while), if you want to see improvements on this project, please consider [sponsoring me](https://github.com/sponsors/egoist). 2 | 3 | 4 | # Docute 5 | 6 | [![npm version](https://badgen.net/npm/v/docute)](https://npm.im/docute) [![jsdelivr downloads](https://data.jsdelivr.com/v1/package/npm/docute/badge?style=rounded)](https://www.jsdelivr.com/package/npm/docute) [![circleci](https://badgen.net/circleci/github/egoist/docute/master)](https://circleci.com/gh/egoist/docute/tree/master) [![donate](https://badgen.net/badge/support%20me/donate/ff69b4)](https://patreon.com/egoist) 7 | 8 | Effortless documentation, done right. 9 | 10 | ## Features 11 | 12 | - No build process, website is generated on the fly. 13 | - A simple yet elegant UI that is dedicated to documentation. 14 | - Leveraging the power of Markdown and Vue. 15 | - Extensible plugin system, plenty of official and community plugins. 16 | 17 | ## Documentation 18 | 19 | - **v4 (latest) docs**: https://docute.egoist.dev 20 | - v3 (legacy) docs: https://docute3.egoist.dev 21 | 22 | ## Resources 23 | 24 | - [Official Plugins](https://github.com/egoist/docute-plugins) 25 | - [Awesome Docute](https://github.com/egoist/awesome-docute) 26 | 27 | ## Contributing 28 | 29 | 1. Fork it! 30 | 2. Create your feature branch: `git checkout -b my-new-feature` 31 | 3. Commit your changes: `git commit -am 'Add some feature'` 32 | 4. Push to the branch: `git push origin my-new-feature` 33 | 5. Submit a pull request :D 34 | 35 | ## Author 36 | 37 | **Docute** © [EGOIST](https://github.com/egoist), Released under the [MIT](./LICENSE) License.
38 | Authored and maintained by EGOIST with help from contributors ([list](https://github.com/egoist/docute/contributors)). 39 | 40 | > [Website](https://egoist.dev) · GitHub [@EGOIST](https://github.com/egoist) · Twitter [@localhost_5173](https://twitter.com/localhost_5173) 41 | -------------------------------------------------------------------------------- /website/docs/zh/plugin-api.md: -------------------------------------------------------------------------------- 1 | # 插件 API 2 | 3 | 插件属性: 4 | 5 | - `name`:`string` 插件名称. 6 | - `extend(api: PluginAPI)`:扩展核心功能 7 | 8 | ## api.processMarkdown(fn) 9 | 10 | - `fn`:`(text: string) => string | Promise ` 11 | 12 | 处理 markdown。 13 | 14 | ## api.extendMarkdownComponent(fn) 15 | 16 | - `fn`: `(Component: VueComponentOptions) => void` 17 | 18 | 修改编译后的 Markdown 组件。 19 | 20 | ## api.processHTML(fn) 21 | 22 | - `fn`:`(html: string) => string | Promise ` 23 | 24 | 处理 HTML. 25 | 26 | ## api.extendMarkedRenderer(fn) 27 | 28 | - `fn`:`(renderer: marked.Renderer) => void` 29 | 30 | 你可以使用 `fn` 来修改我们使用的 [marked 渲染器](https://marked.js.org/#/USING_PRO.md#renderer)。 31 | 32 | ## api.onContentUpdated(fn) 33 | 34 | - `fn`:`(vm: Vue) => void` 35 | 36 | 更新页面内容时将调用 `fn`。 37 | 38 | ## api.registerComponent(position, component) 39 | 40 | - `position`: `string` 41 | - `component`: `VueComponent` 42 | 43 | Register a component at specific position: 44 | 45 | - `sidebar:start`: The start of sidebar. 46 | - `sidebar:end`: The end of sidebar. 47 | - `content:start`: The start of page content. 48 | - `content:end`: The end of page content. 49 | - `header-right:start`: The start of right nav in site header. 50 | - `header-right:end`: The end of right nav in site header. 51 | 52 | ## api.enableSearch(options) 53 | 54 | Enable search bar. 55 | 56 | Properties in `options`: 57 | 58 | |Property|Type|Description| 59 | |---|---|---| 60 | |`handler`|`Handler`|A handler function triggered by every user input.| 61 | 62 | ```ts 63 | type Handler = (keyword: string) => SearchResult[] | Promise 64 | 65 | interface SearchResult { 66 | title: string 67 | link: string 68 | label: string? 69 | description: string? 70 | } 71 | ``` 72 | 73 | ## api.router 74 | 75 | 基本上是 [Vue Router](https://router.vuejs.org/api/#router-instance-properties) 实例。 76 | 77 | ## api.store 78 | 79 | 基本上是 [Vuex](https://vuex.vuejs.org/api/#vuex-store-instance-properties) 实例。 80 | -------------------------------------------------------------------------------- /website/docs/guide/use-vue-in-markdown.md: -------------------------------------------------------------------------------- 1 | # Use Vue in Markdown 2 | 3 | Leverage the power of Vue and JavaScript in writing Markdown document! 4 | 5 | ## Interpolation 6 | 7 | Each markdown file is first compiled into HTML and then rendered as a Vue component. This means you can use Vue-style interpolation in text: 8 | 9 | __Input__: 10 | 11 | ```markdown 12 | {{ 1 + 1 }} 13 | ``` 14 | 15 | __Output__: 16 | 17 | ``` 18 | 2 19 | ``` 20 | 21 | ## Escaping 22 | 23 | If you want to disable Vue-style interpolation in text, you can wrap it inside code fence or inline code as follows: 24 | 25 | __Input__: 26 | 27 | ````markdown 28 | ```js 29 | const foo = `{{ safe, this won't be interpolated! }}` 30 | ``` 31 | 32 | And `{{ bar }}` is safe too! 33 | ```` 34 | 35 | __Output__: 36 | 37 | ```js 38 | const foo = `{{ safe, this won't be interpolated! }}` 39 | ``` 40 | 41 | And `{{ bar }}` is safe too! 42 | 43 | ## Using Components 44 | 45 | Docute exposed the `Vue` constructor on `window` object, so you can use it to register global components in order to use in your Markdown document: 46 | 47 | ```js {highlight:['6-13']} 48 | Vue.component('ReverseText', { 49 | props: ['text'], 50 | template: ` 51 |
52 | {{ reversedText }} 53 | 54 | .reverse-text { 55 | border: 1px solid var(--border-color); 56 | padding: 20px; 57 | font-weight: bold; 58 | border-radius: 4px; 59 | } 60 | 61 |
62 | `, 63 | computed: { 64 | reversedText() { 65 | return this.text.split('').reverse().join('') 66 | } 67 | } 68 | }) 69 | ``` 70 | 71 | You may notice the highlighted part, since you can't directly use `style` tags in Vue template, here we provided the `v-style` component to work around that. Similarly there's also `v-script` component. 72 | 73 | __Input__: 74 | 75 | ```markdown 76 | 77 | ``` 78 | 79 | __Output__: 80 | 81 | 82 | 83 | -------------------------------------------------------------------------------- /src/utils/prismLanguages.json: -------------------------------------------------------------------------------- 1 | { 2 | "javascript": "clike", 3 | "actionscript": "javascript", 4 | "arduino": "cpp", 5 | "aspnet": [ 6 | "markup", 7 | "csharp" 8 | ], 9 | "bison": "c", 10 | "c": "clike", 11 | "csharp": "clike", 12 | "cpp": "c", 13 | "coffeescript": "javascript", 14 | "crystal": "ruby", 15 | "css-extras": "css", 16 | "d": "clike", 17 | "dart": "clike", 18 | "django": "markup", 19 | "erb": [ 20 | "ruby", 21 | "markup-templating" 22 | ], 23 | "fsharp": "clike", 24 | "flow": "javascript", 25 | "glsl": "clike", 26 | "go": "clike", 27 | "groovy": "clike", 28 | "haml": "ruby", 29 | "handlebars": "markup-templating", 30 | "haxe": "clike", 31 | "java": "clike", 32 | "jolie": "clike", 33 | "kotlin": "clike", 34 | "less": "css", 35 | "markdown": "markup", 36 | "markup-templating": "markup", 37 | "n4js": "javascript", 38 | "nginx": "clike", 39 | "objectivec": "c", 40 | "opencl": "cpp", 41 | "parser": "markup", 42 | "php": [ 43 | "clike", 44 | "markup-templating" 45 | ], 46 | "php-extras": "php", 47 | "plsql": "sql", 48 | "processing": "clike", 49 | "protobuf": "clike", 50 | "pug": "javascript", 51 | "qore": "clike", 52 | "jsx": [ 53 | "markup", 54 | "javascript" 55 | ], 56 | "tsx": [ 57 | "jsx", 58 | "typescript" 59 | ], 60 | "reason": "clike", 61 | "ruby": "clike", 62 | "sass": "css", 63 | "scss": "css", 64 | "scala": "java", 65 | "smarty": "markup-templating", 66 | "soy": "markup-templating", 67 | "swift": "clike", 68 | "tap": "yaml", 69 | "textile": "markup", 70 | "tt2": [ 71 | "clike", 72 | "markup-templating" 73 | ], 74 | "twig": "markup", 75 | "typescript": "javascript", 76 | "vbnet": "basic", 77 | "velocity": "markup", 78 | "wiki": "markup", 79 | "xeora": "markup", 80 | "xquery": "markup", 81 | "builtin": [ 82 | "markup", 83 | "xml", 84 | "html", 85 | "mathml", 86 | "svg", 87 | "css", 88 | "clike", 89 | "javascript", 90 | "js" 91 | ] 92 | } -------------------------------------------------------------------------------- /website/docs/zh/README.md: -------------------------------------------------------------------------------- 1 | # Docute 2 | 3 | 一种轻松创建文档的方式。 4 | 5 | ## 什么是 Docute ? 6 | 7 | Docute 本质上就是一个 JavaScript 文件,它可以获取 Markdown 文件并将它们呈现为单页面应用。 8 | 9 | 它完全由运行时驱动,因此并不涉及服务端组件,这就意味着没有构建过程。你只需创建一个 HTML 文件和一堆 Markdown 文档,你的网站就差不多完成了! 10 | 11 | ## 它如何工作? 12 | 13 | 简而言之:URL 是 API。 14 | 15 | 访问你的 URL 时,会获取并呈现相应的 markdown 文件: 16 | 17 | ``` 18 | / => /README.md 19 | /foo => /foo.md 20 | /foo/ => /foo/README.md 21 | /foo/bar => /foo/bar.md 22 | ``` 23 | 24 | ## 快速开始 25 | 26 | 假设你在 `./my-docs` 文件夹中有以下文件: 27 | 28 | ```bash 29 | . 30 | ├── README.md 31 | └── index.html 32 | ``` 33 | 34 | `index.html` 看起来像这样: 35 | 36 | ```html {highlight:[7,'10-16']} 37 | 38 | 39 | 40 | 41 | 42 | My Docs 43 | 44 | 45 | 46 |
47 | 48 | 53 | 54 | 55 | ``` 56 | 57 | 然后你可以使用以下命令将此文件夹作为计算机上的静态网站展示: 58 | 59 | - Node.js: `npm i -g serve && serve .` 60 | - Python: `python -m SimpleHTTPServer` 61 | - Golang: `caddy` 62 | - ..或任何静态 web 服务器 63 | 64 | 下一步, 你可能会想用 [sidebar](./options.md#sidebar), [nav](./options.md#nav) 或其他 [选项](./options.md) 来定制你的文档。 65 | 66 | 这里有一个在线演示,你可以[立刻预览](https://repl.it/@egoist/docute-starter)或者 [下载](https://repl.it/@egoist/docute-starter.zip) 到本地运行。 67 | 68 | ## 比较 69 | 70 | ### VuePress / GitBook / Hexo 71 | 72 | 这些项目在构建时会生成静态的 HTML,这对 SEO 非常有帮助。 73 | 74 | 如果你在意 SEO,那你可能会喜欢使用 [presite](https://github.com/egoist/presite) 来预渲染你的网站。 75 | 76 | ### Docsify 77 | 78 | [Docsify](https://docsify.js.org/#/) 和 Docute 几乎相同,但具有不同的 UI 和不同的使用方式。 79 | 80 | Docute(60kB)比 Docisfy(20kB)大 3 倍,因为我们使用了 Vue,Vue Router 和 Vuex,而 Docsify 使用的是 vanilla JavaScript。 81 | 82 | 83 | ## 浏览器兼容性 84 | 85 | Docute 支持所有常青浏览器(ever-green browsers),即没有对 IE 进行支持! 86 | -------------------------------------------------------------------------------- /website/docs/plugin-api.md: -------------------------------------------------------------------------------- 1 | # Plugin API 2 | 3 | Plugin properties: 4 | 5 | - `name`: `string` Plugin name. 6 | - `extend(api: PluginAPI)`: Extending core features. 7 | 8 | ## api.processMarkdown(fn) 9 | 10 | - `fn`: `(text: string) => string | Promise` 11 | 12 | Process markdown. 13 | 14 | ## api.processHTML(fn) 15 | 16 | - `fn`: `(html: string) => string | Promise ` 17 | 18 | Process HTML. 19 | 20 | ## api.extendMarkedRenderer(fn) 21 | 22 | - `fn`: `(renderer: marked.Renderer) => void` 23 | 24 | You can use `fn` to modify the [marked renderer](https://marked.js.org/#/USING_PRO.md#renderer) we use. 25 | 26 | ## api.extendMarkdownComponent(fn) 27 | 28 | - `fn`: `(Component: VueComponentOptions) => void` 29 | 30 | You can use this hook the modify the compiled markdown component. 31 | 32 | ## api.onContentUpdated(fn) 33 | 34 | - `fn`: `(vm: Vue) => void` 35 | 36 | `fn` will be called when the page content is updated. 37 | 38 | ## api.registerComponent(position, component) 39 | 40 | - `position`: `string` 41 | - `component`: `VueComponent` 42 | 43 | Register a component at specific position: 44 | 45 | - `sidebar:start`: The start of sidebar. 46 | - `sidebar:end`: The end of sidebar. 47 | - `content:start`: The start of page content. 48 | - `content:end`: The end of page content. 49 | - `header-right:start`: The start of right nav in site header. 50 | - `header-right:end`: The end of right nav in site header. 51 | 52 | ## api.enableSearch(options) 53 | 54 | Enable search bar. 55 | 56 | Properties in `options`: 57 | 58 | |Property|Type|Description| 59 | |---|---|---| 60 | |`handler`|`Handler`|A handler function triggered by every user input.| 61 | 62 | ```ts 63 | type Handler = (keyword: string) => SearchResult[] | Promise 64 | 65 | interface SearchResult { 66 | title: string 67 | link: string 68 | label: string? 69 | description: string? 70 | } 71 | ``` 72 | 73 | ## api.router 74 | 75 | Basically the [Vue Router](https://router.vuejs.org/api/#router-instance-properties) instance. 76 | 77 | ## api.store 78 | 79 | Basically the [Vuex](https://vuex.vuejs.org/api/#vuex-store-instance-properties) instance. 80 | -------------------------------------------------------------------------------- /website/docs/guide/deployment.md: -------------------------------------------------------------------------------- 1 | # Deployment 2 | 3 | Keep in mind, it's just a static HTML file that can be served anywhere. 4 | 5 | ## Vercel Recommended 6 | 7 | [Vercel](https://zeit.co) is a platform for Global Static, Jamstack and Serverless Deployments. 8 | 9 | On the Vercel Dashboard, you can import a project from Git Repository, and the only field you need to fill is `output directory`, which should be the directory to your `index.html`. 10 | 11 | 12 | 13 | ## Render 14 | 15 | Render offers [free static site hosting](https://render.com/docs/static-sites) with fully managed SSL, a global CDN and continuous auto deploys from GitHub. 16 | 17 | 1. Create a new Web Service on Render, and give Render’s GitHub app permission to access your new repo. 18 | 2. Use the following values during creation: 19 | - Environment: Static Site 20 | - Build Command: Leave it blank 21 | - Publish Directory: The directory to your `index.html`, for example it should be `docs` if you populated it at `docs/index.html`. 22 | 23 | ## Netlify 24 | 25 | 1. Login to your [Netlify](https://www.netlify.com/) account. 26 | 2. In the [dashboard](https://app.netlify.com/) page, click __New site from Git__. 27 | 3. Choose a repository where you store your docs, leave the __Build Command__ area blank, fill in the __Publish directory__ area with the directory of your `index.html`, for example it should be `docs` if you populated it at `docs/index.html`. 28 | 29 | ## GitHub Pages 30 | 31 | The easiest way to use GitHub Pages is to populate all your files inside `./docs` folder on the master branch, and then use this folder for GitHub Pages: 32 | 33 | 34 | 35 | However you can still use `gh-pages` branch or even `master` branch to serve your docs, it all depends on your needs. 36 | 37 | 38 | 39 | You need to populate a `.nojekyll` file (with empty content) in that folder to disable GitHub Pages' default behaviors for the Jekyll framework (which we don't use at all). 40 | 41 | 42 | -------------------------------------------------------------------------------- /src/components/DocuteSelect.vue: -------------------------------------------------------------------------------- 1 | 21 | 22 | 36 | 37 | 97 | -------------------------------------------------------------------------------- /website/docs/guide/offline-support.md: -------------------------------------------------------------------------------- 1 | # Offline Support 2 | 3 | Improve your website's performance by caching and serving your files, powered by a [service worker](https://developer.mozilla.org/docs/Web/API/Service_Worker_API/Using_Service_Workers). 4 | 5 | First create a `sw.js` in your docs root directory: 6 | 7 | ```js 8 | importScripts( 9 | 'https://storage.googleapis.com/workbox-cdn/releases/3.6.1/workbox-sw.js' 10 | ) 11 | 12 | const ALLOWED_HOSTS = [ 13 | // The domain to load markdown files 14 | location.host, 15 | // The domain to load docute 16 | 'unpkg.com' 17 | ] 18 | 19 | const matchCb = ({ url, event }) => { 20 | return event.request.method === 'GET' && ALLOWED_HOSTS.includes(url.host) 21 | } 22 | 23 | workbox.routing.registerRoute( 24 | matchCb, 25 | workbox.strategies.networkFirst() 26 | ) 27 | ``` 28 | 29 | _[Workbox](https://developers.google.com/web/tools/workbox/) is a library that bakes in a set of best practices and removes the boilerplate every developer writes when working with service workers._ 30 | 31 | Then register this service worker in `index.html`: 32 | 33 | ```html {highlight:['16-18']} 34 | 35 | 36 | 37 | 38 | 42 | My Docs 43 | 44 | 45 | 46 |
47 | 48 | 57 | 58 | 59 | ``` 60 | 61 | __🥳 Now your website will be offline-ready.__ 62 | 63 | If you somehow no longer need this service worker, replace the content of `sw.js` with following code to disable it: 64 | 65 | ```js 66 | self.addEventListener('install', e => { 67 | self.skipWaiting() 68 | }) 69 | 70 | self.addEventListener('activate', e => { 71 | self.registration 72 | .unregister() 73 | .then(() => { 74 | return self.clients.matchAll() 75 | }) 76 | .then(clients => { 77 | clients.forEach(client => client.navigate(client.url)) 78 | }) 79 | }) 80 | ``` 81 | -------------------------------------------------------------------------------- /website/docs/zh/guide/offline-support.md: -------------------------------------------------------------------------------- 1 | # Offline Support 2 | 3 | Improve your website's performance by caching and serving your files, powered by a [service worker](https://developer.mozilla.org/docs/Web/API/Service_Worker_API/Using_Service_Workers). 4 | 5 | First create a `sw.js` in your docs root directory: 6 | 7 | ```js 8 | importScripts( 9 | 'https://storage.googleapis.com/workbox-cdn/releases/3.6.1/workbox-sw.js' 10 | ) 11 | 12 | const ALLOWED_HOSTS = [ 13 | // The domain to load markdown files 14 | location.host, 15 | // The domain to load docute 16 | 'unpkg.com' 17 | ] 18 | 19 | const matchCb = ({ url, event }) => { 20 | return event.request.method === 'GET' && ALLOWED_HOSTS.includes(url.host) 21 | } 22 | 23 | workbox.routing.registerRoute( 24 | matchCb, 25 | workbox.strategies.networkFirst() 26 | ) 27 | ``` 28 | 29 | _[Workbox](https://developers.google.com/web/tools/workbox/) is a library that bakes in a set of best practices and removes the boilerplate every developer writes when working with service workers._ 30 | 31 | Then register this service worker in `index.html`: 32 | 33 | ```html {highlight:['16-18']} 34 | 35 | 36 | 37 | 38 | 42 | My Docs 43 | 44 | 45 | 46 |
47 | 48 | 57 | 58 | 59 | ``` 60 | 61 | __🥳 Now your website will be offline-ready.__ 62 | 63 | If you somehow no longer need this service worker, replace the content of `sw.js` with following code to disable it: 64 | 65 | ```js 66 | self.addEventListener('install', e => { 67 | self.skipWaiting() 68 | }) 69 | 70 | self.addEventListener('activate', e => { 71 | self.registration 72 | .unregister() 73 | .then(() => { 74 | return self.clients.matchAll() 75 | }) 76 | .then(clients => { 77 | clients.forEach(client => client.navigate(client.url)) 78 | }) 79 | }) 80 | ``` 81 | -------------------------------------------------------------------------------- /src/utils/cssVariables.js: -------------------------------------------------------------------------------- 1 | const defaultCssVariables = { 2 | accentColor: '#009688', 3 | pageBackground: '#fff', 4 | headerBackground: '#fff', 5 | headerTextColor: 'var(--text-color)', 6 | textColor: '#000', 7 | linkColor: 'var(--accent-color)', 8 | sidebarWidth: '280px', 9 | sidebarBackground: 'var(--page-background)', 10 | sidebarLinkColor: '#444', 11 | sidebarLinkActiveColor: '#000', 12 | sidebarLinkArrowColor: '#999', 13 | mainBackground: 'var(--page-background)', 14 | borderColor: '#eaeaea', 15 | headerHeight: '55px', 16 | codeFont: 'SFMono-Regular,Consolas,Liberation Mono,Menlo,Courier,monospace', 17 | tipColor: 'rgb(6, 125, 247)', 18 | successColor: '#42b983', 19 | warningColor: '#ff9800', 20 | dangerColor: 'rgb(255, 0, 31)', 21 | navLinkColor: '#2c3e50', 22 | navLinkBorderColor: 'var(--accent-color)', 23 | codeBlockBackground: '#011627', 24 | codeBlockTextColor: 'white', 25 | codeBlockShadowColor: '#333', 26 | codeBlockShadowWidth: '0px', 27 | highlightedLineBackground: '#022a4b', 28 | highlightedLineBorderColor: '#ffa7c4', 29 | inlineCodeColor: 'rgb(116, 66, 16)', 30 | inlineCodeBackground: 'rgb(254, 252, 191)', 31 | loaderPrimaryColor: '#f3f3f3', 32 | loaderSecondaryColor: '#ecebeb', 33 | tableHeaderBackground: '#fafafa', 34 | tableHeaderColor: '#666', 35 | docuteSelectHeight: '38px', 36 | searchIconColor: '#999', 37 | searchFocusBorderColor: '#ccc', 38 | searchFocusIconColor: '#333', 39 | searchResultHoverBackground: '#f9f9f9' 40 | } 41 | 42 | const darkCssVariables = { 43 | ...defaultCssVariables, 44 | headerBackground: 'var(--page-background)', 45 | sidebarLinkColor: 'var(--text-color)', 46 | sidebarLinkActiveColor: 'var(--text-color)', 47 | textColor: 'hsla(0,0%,100%,0.88)', 48 | pageBackground: '#2f3136', 49 | navLinkColor: 'var(--text-color)', 50 | borderColor: '#3e4147', 51 | highlightedLineBackground: '#022a4b', 52 | highlightedLineBorderColor: '#ffa7c4', 53 | inlineCodeColor: '#e6e6e6', 54 | inlineCodeBackground: '#373c49', 55 | loaderPrimaryColor: 'hsla(0, 0%, 100%, 0.08)', 56 | loaderSecondaryColor: 'hsla(0, 0%, 100%, 0.18)', 57 | contentLinkBorder: '2px solid hsla(0, 0%, 100%, 0.28)', 58 | contentLinkHoverBorderColor: 'currentColor', 59 | tableHeaderBackground: 'var(--border-color)', 60 | tableHeaderColor: '#868686', 61 | searchIconColor: '#999', 62 | searchFocusBorderColor: '#999', 63 | searchFocusIconColor: '#ccc', 64 | searchResultBackground: '#27292f', 65 | searchResultHoverBackground: '#1e2025' 66 | } 67 | 68 | export {defaultCssVariables, darkCssVariables} 69 | -------------------------------------------------------------------------------- /website/docs/README.md: -------------------------------------------------------------------------------- 1 | # Introduction to Docute 2 | 3 | The fastest way to create a documentation site for your project. 4 | 5 | ## What is Docute 6 | 7 | Docute is basically a JavaScript file that fetches Markdown files and renders them as a single-page application. 8 | 9 | It's totally runtime-driven so there's no server-side components involved which also means there's no build process. You only need to create an HTML file and a bunch of Markdown documents and your website is almost ready! 10 | 11 | ## How does it work 12 | 13 | In short: URL is the API. 14 | 15 | We fetch and render corresponding markdown file for the URL you visit: 16 | 17 | ``` 18 | / => /README.md 19 | /foo => /foo.md 20 | /foo/ => /foo/README.md 21 | /foo/bar => /foo/bar.md 22 | ``` 23 | 24 | ## Quick Start 25 | 26 | Let's say you have following files in `./my-docs` folder: 27 | 28 | ```bash 29 | . 30 | ├── README.md 31 | └── index.html 32 | ``` 33 | 34 | The `index.html` looks like: 35 | 36 | ```html {highlight:[7,'10-16']} 37 | 38 | 39 | 40 | 41 | 42 | My Docs 43 | 44 | 45 | 46 |
47 | 48 | 53 | 54 | 55 | ``` 56 | 57 | Then you can serve this folder as a static website on your machine using: 58 | 59 | - Node.js: `npm i -g serve && serve .` 60 | - Python: `python -m SimpleHTTPServer` 61 | - Golang: `caddy` 62 | - ..or whatever static web server 63 | 64 | Next, you may want to use [sidebar](./options.md#sidebar), [nav](./options.md#nav) or other [options](./options.md) to customize the website. 65 | 66 | Here's a [REPL](https://repl.it/@egoist/docute-starter) where you can try Docute online or [download](https://repl.it/@egoist/docute-starter.zip) it to run locally. 67 | 68 | ## Comparisons 69 | 70 | ### VuePress / GitBook / Hexo 71 | 72 | They all generate static HTML at build time, which is good for SEO. 73 | 74 | If you care about SEO, you may like using [presite](https://github.com/egoist/presite) to prerender your website. 75 | 76 | ### Docsify 77 | 78 | [Docsify](https://docsify.js.org/#/) and Docute are pretty much the same, but with different UI and different usages. 79 | 80 | Docute (60kB) is 3 times bigger than Docisfy (20kB), because we use Vue, Vue Router and Vuex while Docsify uses vanilla JavaScript under the hood. 81 | 82 | ## Browser Compatibility 83 | 84 | Docute supports all ever-green browsers, i.e. No IE support! 85 | -------------------------------------------------------------------------------- /src/utils/markedRenderer.js: -------------------------------------------------------------------------------- 1 | import marked from './marked' 2 | import {slugify} from '.' 3 | 4 | export default hooks => { 5 | const renderer = new marked.Renderer() 6 | 7 | const slugs = [] 8 | renderer.heading = function(text, level, raw) { 9 | const {env} = this.options 10 | 11 | let slug = slugify(raw) 12 | slugs.push(slug) 13 | const sameSlugCount = slugs.filter(v => v === slug).length 14 | if (sameSlugCount > 1) { 15 | slug += `-${sameSlugCount}` 16 | } 17 | 18 | if (level === 1) { 19 | env.title = text 20 | // Remove h1 header 21 | return '' 22 | } 23 | 24 | if (level === 2) { 25 | env.headings.push({ 26 | level, 27 | raw, 28 | // Remove trailing HTML 29 | text: raw.replace(/<.*>\s*$/g, ''), 30 | slug 31 | }) 32 | } 33 | 34 | const tag = `h${level}` 35 | return `<${tag} class="markdown-header" id="${slug}"> 36 | 37 | 38 | 39 | ${text}` 40 | } 41 | 42 | // Disable template interpolation in code 43 | renderer.codespan = text => `${text}` 44 | const origCode = renderer.code 45 | renderer.code = function(code, lang, escaped, opts) { 46 | opts = opts || {} 47 | const {env} = this.options 48 | 49 | if (opts.mixin) { 50 | env.mixins.push(code) 51 | return '' 52 | } 53 | 54 | let res = origCode.call(this, code, lang, escaped) 55 | 56 | if (!opts.interpolate) { 57 | res = res.replace(/^
/, '
')
58 |     }
59 | 
60 |     if (opts.highlight) {
61 |       const codeMask = code
62 |         .split('\n')
63 |         .map((v, i) => {
64 |           i += 1
65 |           const shouldHighlight = opts.highlight.some(number => {
66 |             if (typeof number === 'number') {
67 |               return number === i
68 |             }
69 |             if (typeof number === 'string') {
70 |               const [start, end] = number.split('-').map(Number)
71 |               return i >= start && (!end || i <= end)
72 |             }
73 |             return false
74 |           })
75 |           const escapedLine = v ? marked.escape(v) : '​'
76 |           return shouldHighlight
77 |             ? `${escapedLine}`
78 |             : `${escapedLine}`
79 |         })
80 |         .join('')
81 |       res += `${codeMask}`
84 |     }
85 | 
86 |     return `
${res}
` 87 | } 88 | 89 | return hooks.process('extendMarkedRenderer', renderer) 90 | } 91 | -------------------------------------------------------------------------------- /src/components/Sidebar.vue: -------------------------------------------------------------------------------- 1 | 26 | 27 | 84 | 85 | 120 | -------------------------------------------------------------------------------- /src/components/Header.vue: -------------------------------------------------------------------------------- 1 | 21 | 22 | 73 | 74 | 144 | -------------------------------------------------------------------------------- /src/index.js: -------------------------------------------------------------------------------- 1 | import Vue from 'vue' 2 | import {sync} from 'vuex-router-sync' 3 | import PluginAPI from './PluginAPI' 4 | import Root from './components/Root.vue' 5 | import store from './store' 6 | import createRouter from './router' 7 | import {inBrowser} from './utils' 8 | import alternativeComponents from './utils/alternativeComponents' 9 | import ImageZoom from './components/ImageZoom.vue' 10 | import Badge from './components/Badge.vue' 11 | import DocuteSelect from './components/DocuteSelect.vue' 12 | import Note from './components/Note.vue' 13 | import Gist from './components/Gist.vue' 14 | import Loading from './components/Loading.vue' 15 | import ExternalLinkIcon from './components/icons/ExternalLinkIcon.vue' 16 | import {INITIAL_STATE_NAME} from './utils/constants' 17 | 18 | // Built-in plugins 19 | import i18nPlugin from './plugins/i18n' 20 | import evaluateContentPlugin from './plugins/evaluateContent' 21 | import versionsPlugin from './plugins/versions' 22 | import bannerFooter from './plugins/banner-footer' 23 | import darkThemeToggler from './plugins/dark-theme-toggler' 24 | import searchPlugin from './plugins/search' 25 | 26 | Vue.component(ImageZoom.name, ImageZoom) 27 | Vue.component(Badge.name, Badge) 28 | Vue.component(DocuteSelect.name, DocuteSelect) 29 | Vue.component(Note.name, Note) 30 | Vue.component(ExternalLinkIcon.name, ExternalLinkIcon) 31 | Vue.component(Gist.name, Gist) 32 | Vue.component(Loading.name, Loading) 33 | Vue.use(alternativeComponents) 34 | 35 | Vue.mixin({ 36 | created() { 37 | const pluginApi = this.$options.pluginApi || this.$root.$pluginApi 38 | if (pluginApi) { 39 | this.$pluginApi = pluginApi 40 | } 41 | } 42 | }) 43 | 44 | class Docute { 45 | constructor(config = {}) { 46 | const router = createRouter(config.router) 47 | sync(store, router) 48 | 49 | this.router = router 50 | this.store = store 51 | 52 | store.commit('SET_CONFIG', { 53 | title: inBrowser && document.title, 54 | ...config 55 | }) 56 | 57 | const plugins = [ 58 | i18nPlugin, 59 | evaluateContentPlugin, 60 | versionsPlugin, 61 | bannerFooter, 62 | darkThemeToggler, 63 | searchPlugin, 64 | ...(store.state.originalConfig.plugins || []) 65 | ] 66 | this.pluginApi = new PluginAPI({plugins, store, router}) 67 | this.applyPlugins() 68 | 69 | this.app = new Vue({ 70 | router, 71 | store, 72 | pluginApi: this.pluginApi, 73 | render: h => h(Root) 74 | }) 75 | 76 | if (config.mount !== false) { 77 | this.mount() 78 | } 79 | } 80 | 81 | mount() { 82 | const {target} = store.getters 83 | // Force hydration when there's initial state 84 | if (window[INITIAL_STATE_NAME]) { 85 | this.app.$mount(`#${target}`, true) 86 | } else { 87 | this.app.$mount(`#${target}`) 88 | } 89 | return this 90 | } 91 | 92 | /** 93 | * @private 94 | */ 95 | applyPlugins() { 96 | for (const plugin of this.pluginApi.plugins) { 97 | plugin.extend(this.pluginApi) 98 | } 99 | } 100 | } 101 | 102 | Docute.version = __DOCUTE_VERSION__ 103 | 104 | export default Docute 105 | 106 | if (typeof window !== 'undefined') { 107 | window.Vue = Vue 108 | // eslint-disable-next-line 109 | window['__DOCUTE_VERSION__'] = __DOCUTE_VERSION__ 110 | } 111 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "docute", 3 | "version": "4.11.0", 4 | "scripts": { 5 | "build:umd": "poi --config build/poi.lib.config.js --prod", 6 | "build:es": "vue-compile src --config build/vue-compile.config.js", 7 | "build": "npm run build:umd && npm run build:es", 8 | "test": "npm run lint && npm run test:unit", 9 | "lint": "xo", 10 | "website": "poi --serve --config build/poi.website.config.js", 11 | "build:website": "poi --prod --config build/poi.website.config.js", 12 | "now-build": "npm run build:website", 13 | "prepublishOnly": "npm run build", 14 | "commit": "git-cz", 15 | "test:unit": "poi puppet src/**/*.test.js --test --plugin @poi/puppet --framework mocha" 16 | }, 17 | "dependencies": { 18 | "jump.js": "^1.0.2", 19 | "loadjs": "^3.5.4", 20 | "marked": "^0.7.0", 21 | "medium-zoom": "^1.0.2", 22 | "prismjs": "^1.15.0", 23 | "throttle-debounce": "^2.1.0", 24 | "vue": "^2.6.10", 25 | "vue-content-loader": "^0.2.1", 26 | "vue-router": "^3.0.1", 27 | "vue-router-prefetch": "^1.1.1", 28 | "vue-template-compiler": "^2.6.10", 29 | "vuex": "^3.0.1", 30 | "vuex-router-sync": "^5.0.0" 31 | }, 32 | "devDependencies": { 33 | "@babel/plugin-syntax-object-rest-spread": "^7.0.0", 34 | "@poi/plugin-puppet": "^0.1.4", 35 | "@semantic-release/git": "^7.0.5", 36 | "babel-preset-minimal": "^0.1.1", 37 | "babel-preset-power-assert": "^3.0.0", 38 | "bili": "^4.8.1", 39 | "commitizen": "^3.0.5", 40 | "cz-conventional-changelog": "^2.1.0", 41 | "docute-google-analytics": "^1.1.0", 42 | "eslint-config-rem": "^3.0.0", 43 | "eslint-plugin-vue": "^5.0.0-beta.3", 44 | "fast-async": "^6.3.8", 45 | "html-template-tag": "^2.0.0", 46 | "husky": "^1.0.1", 47 | "lint-staged": "^7.3.0", 48 | "poi": "^12.4.8", 49 | "postcss-import": "^12.0.0", 50 | "postcss-preset-env": "^6.0.3", 51 | "power-assert": "^1.6.1", 52 | "rollup-plugin-vue": "^4.3.2", 53 | "semantic-release": "^15.12.0", 54 | "vue-compile": "^0.3.1", 55 | "webpack-node-externals": "^1.7.2", 56 | "xo": "^0.23.0" 57 | }, 58 | "repository": { 59 | "url": "egoist/docute", 60 | "type": "git" 61 | }, 62 | "main": "dist/docute.js", 63 | "module": "lib/index.js", 64 | "files": [ 65 | "dist", 66 | "lib", 67 | "!**/__test__/**" 68 | ], 69 | "description": "Effortlessly documentation done right.", 70 | "author": "egoist <0x142857@gmail.com>", 71 | "license": "MIT", 72 | "xo": { 73 | "extends": [ 74 | "rem", 75 | "plugin:vue/essential" 76 | ], 77 | "prettier": true, 78 | "ignores": [ 79 | "**/website/**", 80 | "**/dist/**" 81 | ], 82 | "envs": [ 83 | "browser", 84 | "mocha" 85 | ], 86 | "globals": [ 87 | "__DOCUTE_VERSION__" 88 | ], 89 | "extensions": [ 90 | "vue" 91 | ], 92 | "rules": { 93 | "unicorn/filename-case": "off", 94 | "unicorn/no-abusive-eslint-disable": "off", 95 | "require-atomic-updates": "off" 96 | } 97 | }, 98 | "husky": { 99 | "hooks": { 100 | "pre-commit": "lint-staged" 101 | } 102 | }, 103 | "lint-staged": { 104 | "*.{js,css,md,vue}": [ 105 | "xo --fix", 106 | "git add" 107 | ] 108 | }, 109 | "config": { 110 | "commitizen": { 111 | "path": "./node_modules/cz-conventional-changelog" 112 | } 113 | } 114 | } 115 | -------------------------------------------------------------------------------- /website/docs/zh/guide/customization.md: -------------------------------------------------------------------------------- 1 | # 自定义 2 | 3 | Cusotmizing Docute is as fun as playing with Lego bricks. 4 | 5 | ## 导航栏 6 | 7 | ```js 8 | new Docute({ 9 | title: 'Docute', 10 | nav: [ 11 | { 12 | title: 'Home', 13 | link: '/' 14 | }, 15 | { 16 | title: 'GitHub', 17 | link: 'https://github.com/egoist/docute' 18 | }, 19 | // A dropdown menu 20 | { 21 | title: 'Community', 22 | children: [ 23 | { 24 | title: 'Spectrum', 25 | link: 'https://spectrum.chat/your-community' 26 | }, 27 | { 28 | title: 'Discord', 29 | link: 'https://discord.app/your-discord-server' 30 | } 31 | ] 32 | } 33 | ] 34 | }) 35 | ``` 36 | 37 | `title` 选项的默认值是 `` 标签的内容,因此这个选项不是必需的。 38 | 39 | 显示效果请参考本站的导航栏。 40 | 41 | ## 侧边栏 42 | 43 | 侧边栏一般用于跨页面的导航, 不过正如本站的导航栏,它也显示了一个版本选择器和语言选择器。 44 | 45 | ```js 46 | new Docute({ 47 | sidebar: [ 48 | // A sidebar item, with child links 49 | { 50 | title: 'Guide', 51 | children: [ 52 | { 53 | title: 'Getting Started', 54 | link: '/guide/getting-started' 55 | }, 56 | { 57 | title: 'Installation', 58 | link: '/guide/installation' 59 | } 60 | ] 61 | }, 62 | // An external link 63 | { 64 | title: 'GitHub', 65 | link: 'https://github.com/egoist/docute' 66 | } 67 | ] 68 | }) 69 | ``` 70 | 71 | 查看 [sidebar](../options.md#sidebar) 选项的文档来了解更多细节。 72 | 73 | ## 布局 74 | 75 | Docute 默认使用宽屏布局, 但是也有其他选项: 76 | 77 | <docute-select v-model="$store.state.originalConfig.layout" v-slot="{ value }"> 78 | <option value="wide" :selected="value === 'wide'">Wide</option> 79 | <option value="narrow" :selected="value === 'narrow'">Narrow</option> 80 | <option value="left" :selected="value === 'left'">Left</option> 81 | </docute-select> 82 | 83 | ```js {interpolate:true} 84 | new Docute({ 85 | layout: '{{ $store.state.originalConfig.layout }}' 86 | }) 87 | ``` 88 | 89 | ## 多版本文档 90 | 91 | 假设你的 Git 项目有一个 `master` 分支用于存放最新文档,以及 `v0.1` `v0.2` 分支用于旧版本的文档,你可以用一个 Docute 文档网站来显示多个版本的文档,通过使用 [`overrides`](../options.md#overrides) 和 [`sourcePath`](../options.md#sourcepath) 选项就能办到。 92 | 93 | ```js 94 | // 让这些路径从不同的地方获取 Markdown 文件 95 | overrides: { 96 | '/v0.1/': { 97 | sourcePath: 'https://raw.githubusercontent.com/user/repo/v0.1' 98 | }, 99 | '/v0.2/': { 100 | sourcePath: 'https://raw.githubusercontent.com/user/repo/v0.2' 101 | } 102 | }, 103 | // 用 `versions` 选项在侧边栏添加一个版本选择器 104 | versions: { 105 | 'v1 (Latest)': { 106 | link: '/' 107 | }, 108 | 'v0.2': { 109 | link: '/v0.2/' 110 | }, 111 | 'v0.1': { 112 | link: '/v0.1/' 113 | } 114 | } 115 | ``` 116 | 117 | ## 自定义字体 118 | 119 | Apply custom fonts to your website is pretty easy, you can simply add a `<style>` tag in your HTML file to use [Google Fonts](https://fonts.google.com/): 120 | 121 | ```html 122 | <style> 123 | /* Import desired font from Google fonts */ 124 | @import url('https://fonts.googleapis.com/css?family=Lato'); 125 | 126 | /* Apply the font to body (to override the default one) */ 127 | body { 128 | font-family: Lato, sans-serif; 129 | } 130 | </style> 131 | ``` 132 | 133 | <button @click="insertCustomFontsCSS">Click me</button> to toggle the custom fonts on this website. 134 | 135 | By default a fresh Docute website will use system default fonts. 136 | 137 | ## 自定义样式 138 | 139 | You can use [`cssVariables`](../options.md#cssvariables) option to customize site style: 140 | 141 | ```js 142 | new Docute({ 143 | cssVariables: { 144 | sidebarWidth: '300px' 145 | } 146 | }) 147 | 148 | // Or using a function to get current theme 149 | new Docute({ 150 | cssVariables(theme) { 151 | return theme === 'dark' ? {} : {} 152 | } 153 | }) 154 | ``` 155 | 156 | The `cssVariables` used by the the <code>{{ $store.getters.config.theme }}</code> theme: 157 | 158 | <ul> 159 | <li v-for="(value, key) in $store.getters.cssVariables" :key="key"> 160 | <strong>{{key}}</strong>: <color-box :color="value" v-if="/(Color|Background)/.test(key)" /> 161 | <code>{{value}}</code> 162 | </li> 163 | </ul> 164 | 165 | Note that these properties are defined in camelCase but you should reference them in CSS using kebab-case: 166 | 167 | ```css 168 | .Sidebar { 169 | width: var(--sidebar-width); 170 | } 171 | ``` 172 | -------------------------------------------------------------------------------- /src/components/HeaderNav.vue: -------------------------------------------------------------------------------- 1 | <template> 2 | <div class="header-nav"> 3 | <div class="header-nav-item" v-for="(item, index) in nav" :key="index"> 4 | <div class="dropdown-wrapper" v-if="item.children"> 5 | <span class="dropdown-trigger"> 6 | {{ item.title }} 7 | <span class="arrow"></span> 8 | </span> 9 | <ul class="dropdown-list" v-if="item.children"> 10 | <li 11 | class="dropdown-item" 12 | v-for="(childItem, index) in item.children" 13 | :key="index" 14 | > 15 | <uni-link 16 | :to="childItem.link" 17 | :openInNewTab="childItem.openInNewTab" 18 | :externalLinkIcon="false" 19 | >{{ childItem.title }}</uni-link 20 | > 21 | </li> 22 | </ul> 23 | </div> 24 | 25 | <uni-link 26 | v-if="!item.children" 27 | :to="item.link" 28 | :openInNewTab="item.openInNewTab" 29 | :externalLinkIcon="false" 30 | >{{ item.title }}</uni-link 31 | > 32 | </div> 33 | </div> 34 | </template> 35 | 36 | <script> 37 | import {isExternalLink} from '../utils' 38 | import UniLink from './UniLink.vue' 39 | 40 | export default { 41 | components: { 42 | UniLink 43 | }, 44 | 45 | props: { 46 | nav: { 47 | type: Array, 48 | required: true 49 | } 50 | }, 51 | 52 | methods: { 53 | isExternalLink 54 | } 55 | } 56 | </script> 57 | 58 | <style scoped> 59 | .header-nav { 60 | display: flex; 61 | align-items: center; 62 | font-size: 1rem; 63 | 64 | @media (max-width: 768px) { 65 | display: none; 66 | } 67 | } 68 | 69 | /deep/ a { 70 | color: var(--nav-link-color); 71 | } 72 | 73 | .header-nav-item { 74 | height: 100%; 75 | 76 | &:not(:first-child) { 77 | margin-left: 25px; 78 | } 79 | 80 | & > /deep/ a { 81 | display: inline-flex; 82 | align-items: center; 83 | line-height: 1.4; 84 | height: 100%; 85 | position: relative; 86 | 87 | &:after { 88 | content: ''; 89 | height: 2px; 90 | width: 100%; 91 | position: absolute; 92 | bottom: 0; 93 | left: 0; 94 | right: 0; 95 | display: block; 96 | } 97 | 98 | &.router-link-exact-active { 99 | color: var(--accent-color); 100 | &:after { 101 | background-color: var(--nav-link-border-color); 102 | } 103 | } 104 | } 105 | } 106 | 107 | .mobile-header-nav { 108 | display: block; 109 | padding: 0 20px; 110 | margin-bottom: 30px; 111 | padding-bottom: 30px; 112 | border-bottom: 1px solid var(--border-color); 113 | 114 | & .header-nav-item { 115 | &:not(:first-child) { 116 | margin-left: 0; 117 | } 118 | } 119 | 120 | @media (min-width: 768px) { 121 | display: none; 122 | } 123 | } 124 | 125 | .arrow { 126 | display: inline-block; 127 | vertical-align: middle; 128 | margin-top: -1px; 129 | margin-left: 6px; 130 | margin-right: -14px; 131 | width: 0; 132 | height: 0; 133 | border-left: 4px solid transparent; 134 | border-right: 4px solid transparent; 135 | border-top: 5px solid #ccc; 136 | } 137 | 138 | .dropdown-wrapper { 139 | position: relative; 140 | 141 | &:hover .dropdown-list { 142 | display: block; 143 | } 144 | } 145 | 146 | .dropdown-trigger { 147 | &:hover { 148 | cursor: default; 149 | } 150 | } 151 | 152 | .dropdown-list { 153 | display: none; 154 | list-style: none; 155 | margin: 0; 156 | padding: 5px 0; 157 | padding-left: 0; 158 | border: 1px solid var(--border-color); 159 | border-radius: 4px; 160 | position: absolute; 161 | right: 0; 162 | top: 100%; 163 | background: var(--header-background); 164 | 165 | @media (max-width: 768px) { 166 | position: relative; 167 | } 168 | } 169 | 170 | .dropdown-item { 171 | line-height: 1.6; 172 | 173 | & /deep/ a { 174 | padding: 2px 1.5rem 2px 1.25rem; 175 | white-space: nowrap; 176 | display: block; 177 | position: relative; 178 | 179 | &.router-link-exact-active { 180 | color: var(--accent-color); 181 | 182 | &:before { 183 | content: ''; 184 | width: 0; 185 | height: 0; 186 | border-left: 5px solid #3eaf7c; 187 | border-top: 3px solid transparent; 188 | border-bottom: 3px solid transparent; 189 | position: absolute; 190 | top: calc(50% - 2px); 191 | left: 9px; 192 | } 193 | } 194 | } 195 | } 196 | </style> 197 | -------------------------------------------------------------------------------- /src/plugins/search/SearchBar.vue: -------------------------------------------------------------------------------- 1 | <template> 2 | <div class="search" :class="{'is-focused': focused}" v-if="enabled"> 3 | <div class="search-input-wrapper"> 4 | <span class="search-icon"> 5 | <svg 6 | width="13" 7 | height="13" 8 | viewBox="0 0 13 13" 9 | xmlns="http://www.w3.org/2000/svg" 10 | fill="currentColor" 11 | > 12 | <path 13 | d="M8.87 8.16l3.25 3.25-.7.71-3.26-3.25a5 5 0 1 1 .7-.7zM5 9a4 4 0 1 0 0-8 4 4 0 0 0 0 8z" 14 | ></path> 15 | </svg> 16 | </span> 17 | <input 18 | class="search-input" 19 | type="text" 20 | @input="handleSearch" 21 | @focus="toggleFocus(true)" 22 | /> 23 | <div class="search-result" ref="result" v-show="result.length > 0"> 24 | <router-link 25 | :to="item.link" 26 | class="search-result-item" 27 | v-for="(item, i) in result" 28 | :key="i" 29 | > 30 | <div class="item-header"> 31 | <div class="item-title" v-html="item.title"></div> 32 | <span class="item-label" v-if="item.label">{{ item.label }}</span> 33 | </div> 34 | <div class="item-desc" v-html="item.description"></div> 35 | </router-link> 36 | </div> 37 | </div> 38 | </div> 39 | </template> 40 | 41 | <script> 42 | import {debounce} from 'throttle-debounce' 43 | 44 | export default { 45 | data() { 46 | return { 47 | result: [], 48 | focused: false 49 | } 50 | }, 51 | 52 | watch: { 53 | '$route.fullPath'() { 54 | this.focused = false 55 | } 56 | }, 57 | 58 | mounted() { 59 | document.addEventListener('click', this.handleClick) 60 | }, 61 | 62 | beforeDestroy() { 63 | document.removeEventListener('click', this.handleClick) 64 | }, 65 | 66 | computed: { 67 | enabled() { 68 | return this.$pluginApi.search.enabled 69 | } 70 | }, 71 | 72 | methods: { 73 | handleClick(e) { 74 | if ( 75 | !this.$el.contains(e.target) || 76 | this.$refs.result.contains(e.target) 77 | ) { 78 | this.focused = false 79 | } 80 | }, 81 | 82 | handleSearch: debounce(300, async function(e) { 83 | const {handler} = this.$pluginApi.search 84 | this.result = await handler(e.target.value) 85 | }), 86 | 87 | toggleFocus(focused) { 88 | this.focused = focused 89 | } 90 | } 91 | } 92 | </script> 93 | 94 | <style scoped> 95 | .search { 96 | display: flex; 97 | height: 100%; 98 | align-items: center; 99 | position: relative; 100 | 101 | &.is-focused { 102 | & .search-icon { 103 | color: var(--search-focus-icon-color); 104 | } 105 | 106 | & .search-input-wrapper { 107 | border-color: var(--search-focus-border-color); 108 | } 109 | 110 | & .search-result { 111 | display: block; 112 | } 113 | } 114 | 115 | @media print { 116 | display: none; 117 | } 118 | } 119 | 120 | .search-input-wrapper { 121 | border: 1px solid var(--border-color); 122 | border-radius: 4px; 123 | padding: 0; 124 | height: 50%; 125 | position: relative; 126 | width: 240px; 127 | 128 | @media (max-width: 768px) { 129 | width: 28px; 130 | overflow: hidden; 131 | 132 | @nest .is-focused & { 133 | width: 200px; 134 | overflow: initial; 135 | } 136 | } 137 | } 138 | 139 | .search-icon { 140 | color: var(--search-icon-color); 141 | position: absolute; 142 | top: 50%; 143 | left: 7px; 144 | transform: translateY(-50%); 145 | margin-top: 2px; 146 | } 147 | 148 | .search-input { 149 | border: none; 150 | outline: none; 151 | background: transparent; 152 | color: var(--text-color); 153 | position: absolute; 154 | padding: 0 8px 0 28px; 155 | width: 100%; 156 | height: 100%; 157 | } 158 | 159 | .search-result { 160 | display: none; 161 | position: absolute; 162 | top: calc(100% + 8px); 163 | right: 0; 164 | width: 20rem; 165 | border-radius: 4px; 166 | overflow: hidden; 167 | box-shadow: 0px 10px 30px rgba(0, 0, 0, 0.12); 168 | z-index: 9999; 169 | background: var(--header-background); 170 | } 171 | 172 | .search-result-item { 173 | padding: 10px; 174 | display: block; 175 | background: var(--search-result-background); 176 | 177 | &:not(:last-child) { 178 | border-bottom: 1px solid var(--border-color); 179 | } 180 | 181 | &:hover { 182 | background: var(--search-result-hover-background); 183 | } 184 | } 185 | 186 | .item-title { 187 | font-size: 1.1rem; 188 | font-weight: 500; 189 | display: inline; 190 | line-height: 1; 191 | } 192 | 193 | .item-desc { 194 | font-size: 0.875rem; 195 | margin-top: 10px; 196 | } 197 | 198 | .item-label { 199 | border-radius: 4px; 200 | padding: 0 5px; 201 | height: 22px; 202 | display: inline-block; 203 | border: 1px solid var(--border-color); 204 | font-size: 13px; 205 | margin-left: 10px; 206 | } 207 | </style> 208 | -------------------------------------------------------------------------------- /src/css/page-content.css: -------------------------------------------------------------------------------- 1 | .page-content { 2 | & > *:first-child { 3 | margin-top: 0; 4 | } 5 | 6 | &.has-page-title > h2:first-child { 7 | margin-top: 7rem; 8 | } 9 | 10 | & h1, 11 | & h2, 12 | & h3, 13 | & h4, 14 | & h5, 15 | & h6 { 16 | font-weight: 300; 17 | line-height: 1.2; 18 | } 19 | 20 | & h1 { 21 | font-size: 3rem; 22 | margin-bottom: 1.4rem; 23 | } 24 | 25 | & h2 { 26 | font-size: 2rem; 27 | border-bottom: 1px solid var(--border-color); 28 | margin-top: 7rem; 29 | padding-bottom: 5px; 30 | } 31 | 32 | & h3 { 33 | font-size: 1.7rem; 34 | margin: 40px 0 30px 0; 35 | } 36 | 37 | & h4 { 38 | font-size: 1.4rem; 39 | } 40 | 41 | & h5 { 42 | font-size: 1.1rem; 43 | } 44 | 45 | & p { 46 | margin: 15px 0; 47 | } 48 | 49 | & table { 50 | width: 100%; 51 | border-spacing: 0; 52 | border-collapse: separate; 53 | } 54 | 55 | & table th, 56 | & table td { 57 | padding: 12px 10px; 58 | border-bottom: 1px solid var(--border-color); 59 | text-align: left; 60 | } 61 | 62 | & thead th { 63 | color: var(--table-header-color); 64 | background: var(--table-header-background); 65 | border-bottom: 1px solid var(--border-color); 66 | border-top: 1px solid var(--border-color); 67 | font-weight: 400; 68 | font-size: 12px; 69 | padding: 10px; 70 | 71 | &:first-child { 72 | border-left: 1px solid var(--border-color); 73 | border-radius: 4px 0px 0px 4px; 74 | } 75 | 76 | &:last-child { 77 | border-right: 1px solid var(--border-color); 78 | border-radius: 0 4px 4px 0; 79 | } 80 | } 81 | 82 | & .pre-wrapper { 83 | margin: 2rem 0; 84 | position: relative; 85 | border-radius: 4px; 86 | background: var(--code-block-background); 87 | box-shadow: inset 0 0 0 var(--code-block-shadow-width) var(--code-block-shadow-color); 88 | 89 | &:before { 90 | content: attr(data-lang); 91 | position: absolute; 92 | top: 5px; 93 | right: 10px; 94 | font-size: 12px; 95 | color: #cacaca; 96 | } 97 | 98 | & code { 99 | color: var(--code-block-text-color); 100 | } 101 | } 102 | 103 | & pre, 104 | & .code-mask { 105 | overflow: auto; 106 | position: relative; 107 | margin: 0; 108 | z-index: 2; 109 | font-family: var(--code-font); 110 | white-space: pre; 111 | 112 | & code { 113 | box-shadow: none; 114 | margin: 0; 115 | padding: 0; 116 | border: none; 117 | font-size: 1em; 118 | background: transparent; 119 | } 120 | 121 | @media print { 122 | white-space: pre-wrap; 123 | word-break: break-word; 124 | } 125 | } 126 | 127 | & pre { 128 | padding: 20px; 129 | } 130 | 131 | & .code-mask { 132 | position: absolute; 133 | top: 0; 134 | left: 0; 135 | right: 0; 136 | z-index: 1; 137 | padding-top: 20px; 138 | border: none; 139 | color: transparent; 140 | } 141 | 142 | & .code-line { 143 | display: block; 144 | padding: 0 20px; 145 | &.highlighted { 146 | background: var(--highlighted-line-background); 147 | position: relative; 148 | &:before { 149 | content: ''; 150 | display: block; 151 | width: 3px; 152 | top: 0; 153 | left: 0; 154 | bottom: 0; 155 | background: var(--highlighted-line-border-color); 156 | position: absolute; 157 | } 158 | } 159 | } 160 | 161 | & code { 162 | font-family: var(--code-font); 163 | font-size: 90%; 164 | background: var(--inline-code-background); 165 | border-radius: 4px; 166 | padding: 3px 5px; 167 | color: var(--inline-code-color); 168 | } 169 | 170 | & > ul, 171 | & > ol { 172 | padding-left: 20px; 173 | margin: 1rem 0; 174 | } 175 | 176 | & .contains-task-list { 177 | list-style: none; 178 | padding-left: 0; 179 | } 180 | 181 | & img { 182 | max-width: 100%; 183 | } 184 | 185 | & blockquote { 186 | background: #f1f1f1; 187 | border-left: 8px solid #ccc; 188 | margin: 20px 0; 189 | padding: 14px 16px; 190 | color: #6a737d; 191 | 192 | & p { 193 | margin: 15px 0 0 0; 194 | } 195 | 196 | & > *:first-child { 197 | margin-top: 0; 198 | } 199 | } 200 | 201 | & hr { 202 | height: 1px; 203 | padding: 0; 204 | margin: 3rem 0; 205 | background-color: #e1e4e8; 206 | border: 0; 207 | } 208 | 209 | & .header-anchor { 210 | float: left; 211 | line-height: 1; 212 | margin-left: -20px; 213 | padding-right: 4px; 214 | opacity: 0; 215 | border-bottom: none; 216 | 217 | &:hover { 218 | opacity: 1; 219 | border-bottom: none; 220 | } 221 | 222 | & .anchor-icon { 223 | vertical-align: middle; 224 | fill: currentColor; 225 | } 226 | } 227 | 228 | & .markdown-header:focus, 229 | & .markdown-header:hover { 230 | outline: none; 231 | & .header-anchor { 232 | opacity: 1; 233 | } 234 | } 235 | } 236 | -------------------------------------------------------------------------------- /website/docs/guide/customization.md: -------------------------------------------------------------------------------- 1 | # Customization 2 | 3 | Customizing Docute is as fun as playing with Lego bricks. 4 | 5 | ## Navbar 6 | 7 | The navbar is used for site-level navigation. It usually contains a link to your homepage and a link to your project's repository. However you can add whatever you want there. 8 | 9 | ```js 10 | new Docute({ 11 | title: 'Docute', 12 | nav: [ 13 | { 14 | title: 'Home', 15 | link: '/' 16 | }, 17 | { 18 | title: 'GitHub', 19 | link: 'https://github.com/egoist/docute' 20 | }, 21 | // A dropdown menu 22 | { 23 | title: 'Community', 24 | children: [ 25 | { 26 | title: 'Spectrum', 27 | link: 'https://spectrum.chat/your-community' 28 | }, 29 | { 30 | title: 'Discord', 31 | link: 'https://discord.app/your-discord-server' 32 | } 33 | ] 34 | } 35 | ] 36 | }) 37 | ``` 38 | 39 | The `title` option defaults to the value of `<title>` tag in your HTML, so it's completely optional. 40 | 41 | Check out the navbar of this website to see how it looks. 42 | 43 | ## Sidebar 44 | 45 | Sidebar is mainly used for navigations between pages. As you can see from this page, we also use it to display a version selector and a language selector. 46 | 47 | ```js 48 | new Docute({ 49 | sidebar: [ 50 | // A sidebar item, with child links 51 | { 52 | title: 'Guide', 53 | children: [ 54 | { 55 | title: 'Getting Started', 56 | link: '/guide/getting-started' 57 | }, 58 | { 59 | title: 'Installation', 60 | link: '/guide/installation' 61 | } 62 | ] 63 | }, 64 | // An external link 65 | { 66 | title: 'GitHub', 67 | link: 'https://github.com/egoist/docute' 68 | } 69 | ] 70 | }) 71 | ``` 72 | 73 | Check out the [sidebar](../options.md#sidebar) option reference for more details. 74 | 75 | ## Layout 76 | 77 | Docute by default uses a wide-screen layout as you see, but there're more layouts available: 78 | 79 | <docute-select v-model="$store.state.originalConfig.layout" v-slot="{ value }"> 80 | <option value="wide" :selected="value === 'wide'">Wide</option> 81 | <option value="narrow" :selected="value === 'narrow'">Narrow</option> 82 | <option value="left" :selected="value === 'left'">Left</option> 83 | </docute-select> 84 | 85 | ```js {interpolate:true} 86 | new Docute({ 87 | layout: '{{ $store.state.originalConfig.layout }}' 88 | }) 89 | ``` 90 | 91 | ## Versioning 92 | 93 | Let's say you have `master` branch for the latest docs and `v0.1` `v0.2` branches for older versions, you can use one Docute website to serve them all, with the help of [`overrides`](../options.md#overrides) and [`sourcePath`](../options.md#sourcepath) option. 94 | 95 | ```js 96 | new Docute({ 97 | // Configure following paths to load Markdown files from different path 98 | overrides: { 99 | '/v0.1/': { 100 | sourcePath: 'https://raw.githubusercontent.com/user/repo/v0.1' 101 | }, 102 | '/v0.2/': { 103 | sourcePath: 'https://raw.githubusercontent.com/user/repo/v0.2' 104 | } 105 | }, 106 | // Use `versions` option to add a version selector 107 | // In the sidebar 108 | versions: { 109 | 'v1 (Latest)': { 110 | link: '/' 111 | }, 112 | 'v0.2': { 113 | link: '/v0.2/' 114 | }, 115 | 'v0.1': { 116 | link: '/v0.1/' 117 | } 118 | } 119 | }) 120 | ``` 121 | 122 | ## Custom Fonts 123 | 124 | Apply custom fonts to your website is pretty easy, you can simply add a `<style>` tag in your HTML file to use [Google Fonts](https://fonts.google.com/): 125 | 126 | ```html 127 | <style> 128 | /* Import desired font from Google fonts */ 129 | @import url('https://fonts.googleapis.com/css?family=Lato'); 130 | 131 | /* Apply the font to body (to override the default one) */ 132 | body { 133 | font-family: Lato, sans-serif; 134 | } 135 | </style> 136 | ``` 137 | 138 | <button @click="insertCustomFontsCSS">Click me</button> to toggle the custom fonts on this website. 139 | 140 | By default a fresh Docute website will use system default fonts. 141 | 142 | ## Custom Style 143 | 144 | You can use [`cssVariables`](../options.md#cssvariables) option to customize site style: 145 | 146 | ```js 147 | new Docute({ 148 | cssVariables: { 149 | sidebarWidth: '300px' 150 | } 151 | }) 152 | 153 | // Or using a function to get current theme 154 | new Docute({ 155 | cssVariables(theme) { 156 | return theme === 'dark' ? {} : {} 157 | } 158 | }) 159 | ``` 160 | 161 | The `cssVariables` used by the the <code>{{ $store.getters.config.theme }}</code> theme: 162 | 163 | <ul> 164 | <li v-for="(value, key) in $store.getters.cssVariables" :key="key"> 165 | <strong>{{key}}</strong>: <color-box :color="value" v-if="/(Color|Background)/.test(key)" /> 166 | <code>{{value}}</code> 167 | </li> 168 | </ul> 169 | 170 | Note that these properties are defined in camelCase but you should reference them in CSS using kebab-case: 171 | 172 | ```css 173 | .Sidebar { 174 | width: var(--sidebar-width); 175 | } 176 | ``` 177 | -------------------------------------------------------------------------------- /src/components/SidebarItem.vue: -------------------------------------------------------------------------------- 1 | <template> 2 | <div :class="['SidebarItem', item.title && 'hasTitle']"> 3 | <div 4 | class="ItemTitle" 5 | :class="{collapsable: item.collapsable !== false}" 6 | v-if="item.title && children" 7 | @click="$emit('toggle')" 8 | > 9 | <span v-if="item.collapsable !== false" class="arrow" :class="{open}"> 10 | <svg 11 | width="6" 12 | height="10" 13 | viewBox="0 0 6 10" 14 | fill="none" 15 | xmlns="http://www.w3.org/2000/svg" 16 | > 17 | <path 18 | d="M1.4 8.56L4.67 5M1.4 1.23L4.66 4.7" 19 | stroke="currentColor" 20 | stroke-linecap="square" 21 | /> 22 | </svg> 23 | </span> 24 | <span>{{ item.title }}</span> 25 | </div> 26 | <uni-link 27 | class="ItemLink" 28 | :class="{active: $route.path === item.link}" 29 | v-if="item.title && item.link" 30 | :to="item.link" 31 | >{{ item.title }}</uni-link 32 | > 33 | <div class="ItemLinkToc" v-if="item.title && item.link"> 34 | <PageToc :link="item" /> 35 | </div> 36 | 37 | <div 38 | class="ItemChildren" 39 | v-if="children && (open || item.collapsable === false)" 40 | > 41 | <div class="ItemChild" v-for="(link, index) of children" :key="index"> 42 | <uni-link 43 | class="ItemChildLink" 44 | :class="{active: $route.path === link.link}" 45 | :to="link.link" 46 | :openInNewTab="link.openInNewTab" 47 | :prefetchFiles="getPrefetchFiles(link.link)" 48 | >{{ link.title }}</uni-link 49 | > 50 | <PageToc :link="link" /> 51 | </div> 52 | </div> 53 | </div> 54 | </template> 55 | 56 | <script> 57 | import {isExternalLink, getFileUrl, getFilenameByPath} from '../utils' 58 | import UniLink from './UniLink.vue' 59 | import PageToc from './PageToc.vue' 60 | 61 | export default { 62 | components: { 63 | UniLink, 64 | PageToc 65 | }, 66 | props: { 67 | item: { 68 | type: Object, 69 | required: true, 70 | default() { 71 | return {} 72 | } 73 | }, 74 | open: { 75 | type: Boolean, 76 | required: false, 77 | default() { 78 | return true 79 | } 80 | } 81 | }, 82 | computed: { 83 | children() { 84 | return this.item.children || this.item.links 85 | } 86 | }, 87 | methods: { 88 | isExternalLink, 89 | 90 | getPrefetchFiles(path) { 91 | const {sourcePath, routes} = this.$store.getters.config 92 | if (routes && routes[path]) { 93 | const {file} = routes[path] 94 | return file ? [file] : [] 95 | } 96 | const filename = getFilenameByPath(path) 97 | const fileUrl = getFileUrl(sourcePath, filename) 98 | return fileUrl ? [fileUrl] : [] 99 | }, 100 | 101 | getLinkTarget(link) { 102 | if (!isExternalLink(link) || link.openInNewTab === false) { 103 | return '_self' 104 | } 105 | return '_blank' 106 | } 107 | } 108 | } 109 | </script> 110 | 111 | <style scoped> 112 | .SidebarItem { 113 | &:not(:last-child) { 114 | margin-bottom: 10px; 115 | } 116 | 117 | font-size: 0.875rem; 118 | 119 | & /deep/ a { 120 | color: var(--sidebar-link-color); 121 | 122 | &:hover { 123 | color: var(--sidebar-link-active-color); 124 | } 125 | } 126 | } 127 | 128 | .ItemTitle { 129 | padding: 0 20px; 130 | margin-bottom: 10px; 131 | position: relative; 132 | color: var(--sidebar-link-color); 133 | user-select: none; 134 | font-size: 0; 135 | 136 | &.collapsable:hover { 137 | cursor: pointer; 138 | color: var(--sidebar-link-active-color); 139 | } 140 | 141 | & span { 142 | font-size: 0.9rem; 143 | } 144 | } 145 | 146 | .ItemLink { 147 | margin: 0 20px; 148 | display: flex; 149 | align-items: center; 150 | 151 | &:before { 152 | content: ''; 153 | display: block; 154 | width: 4px; 155 | height: 4px; 156 | border-radius: 50%; 157 | background-color: var(--sidebar-link-arrow-color); 158 | margin-right: 8px; 159 | } 160 | 161 | &.active { 162 | color: var(--sidebar-link-active-color); 163 | font-weight: bold; 164 | } 165 | } 166 | 167 | .ItemLinkToc { 168 | margin: 0 8px; 169 | } 170 | 171 | .ItemChildren { 172 | border-left: 1px solid var(--border-color); 173 | margin: 0 20px; 174 | } 175 | 176 | .ItemChild { 177 | &:not(:last-child) { 178 | margin-bottom: 10px; 179 | } 180 | } 181 | 182 | .ItemChildLink { 183 | padding-left: 16px; 184 | display: flex; 185 | position: relative; 186 | line-height: 1; 187 | 188 | &.active { 189 | font-weight: bold; 190 | } 191 | } 192 | 193 | a { 194 | text-decoration: none; 195 | color: var(--text-color); 196 | } 197 | 198 | .arrow { 199 | width: 16px; 200 | display: inline-block; 201 | color: var(--sidebar-link-arrow-color); 202 | 203 | & svg { 204 | transition: all 0.15s ease; 205 | } 206 | 207 | &.open { 208 | & svg { 209 | transform: rotate(90deg); 210 | } 211 | } 212 | } 213 | </style> 214 | -------------------------------------------------------------------------------- /website/docs/zh/guide/markdown-features.md: -------------------------------------------------------------------------------- 1 | # 撰写 2 | 3 | 文档应易于阅读且易于撰写。 4 | 5 | ## 文档规范 6 | 7 | 文档应以 Markdown 格式展现: 8 | 9 | ```markdown 10 | # 标题 11 | 12 | 内容填在这里... 13 | ``` 14 | 15 | 如果你不知道它是什么,请查阅 [Markdown](https://daringfireball.net/projects/markdown/)。 16 | 17 | ## 链接 18 | 19 | ### 内部链接 20 | 21 | 内部链接会转换为 `<router-link>` 进行 SPA 式导航。 22 | 23 | __输入__: 24 | 25 | ```markdown 26 | - [首页](/zh/) <!-- 展示首页 --> 27 | - [在 Markdown 中使用 Vue](/zh/guide/use-vue-in-markdown) <!-- 展示其他页面 --> 28 | - [查看 `title` 选项](../options.md#title) <!-- 甚至是链接到一个内部 Markdown 文件 --> 29 | ``` 30 | 31 | __输出__: 32 | 33 | - [首页](/zh/) <!-- 展示首页 --> 34 | - [在 Markdown 中使用 Vue](/zh/guide/use-vue-in-markdown) <!-- 展示其他页面 --> 35 | - [查看 `title` 选项](../options.md#title) <!-- 甚至是链接到一个内部 Markdown 文件 --> 36 | 37 | ### 外部链接 38 | 39 | 外部链接会自动添加 HTML 属性 `target="_blank" rel="noopener noreferrer"`,例如: 40 | 41 | __输入__: 42 | 43 | ```markdown 44 | - [Docute website](https://docute.org) 45 | - [Docute repo](https://github.com/egoist/docute) 46 | ``` 47 | 48 | __输出__: 49 | 50 | - [Docute website](https://docute.org) 51 | - [Docute repo](https://github.com/egoist/docute) 52 | 53 | ## 任务列表 54 | 55 | __输入__: 56 | 57 | ```markdown 58 | - [ ] Rule the web 59 | - [x] Conquer the world 60 | - [ ] Learn Docute 61 | ``` 62 | 63 | __输出__: 64 | 65 | - [ ] Rule the web 66 | - [x] Conquer the world 67 | - [ ] Learn Docute 68 | 69 | ## 代码高亮 Highlighting 70 | 71 | 代码框使用 [Prism.js](https://prismjs.com/) 高亮显示,示例代码: 72 | 73 | ```js 74 | // Returns a function, that, as long as it continues to be invoked, will not 75 | // be triggered. The function will be called after it stops being called for 76 | // N milliseconds. If `immediate` is passed, trigger the function on the 77 | // leading edge, instead of the trailing. 78 | function debounce(func, wait, immediate) { 79 | var timeout; 80 | return function() { 81 | var context = this, args = arguments; 82 | var later = function() { 83 | timeout = null; 84 | if (!immediate) func.apply(context, args); 85 | }; 86 | var callNow = immediate && !timeout; 87 | clearTimeout(timeout); 88 | timeout = setTimeout(later, wait); 89 | if (callNow) func.apply(context, args); 90 | }; 91 | }; 92 | ``` 93 | 94 | 默认支持的语言: 95 | 96 | <ul> 97 | <li v-for="lang in builtinLanguages" :key="lang"> 98 | {{ lang }} 99 | </li> 100 | </ul> 101 | 102 | 你可以查看[高亮](/zh/options#highlight)选项添加更多语言。 103 | 104 | ## Code Fence Options 105 | 106 | Next to the code fence language, you can use a JS object to specify options: 107 | 108 | ````markdown 109 | ```js {highlightLines: [2]} 110 | function foo() { 111 | console.log('foo') 112 | } 113 | ``` 114 | ```` 115 | 116 | Available options: 117 | 118 | - `highlightLines`: [Line Highlighting in Code Fences](#line-highlighting-in-code-fences) 119 | - `mixin`: [Adding Vue Mixin](#adding-vue-mixin) 120 | 121 | ## 代码框中某行高亮显示 122 | 123 | __输入:__ 124 | 125 | ````markdown 126 | ```js {highlight:[3,'5-7',12]} 127 | class SkinnedMesh extends THREE.Mesh { 128 | constructor(geometry, materials) { 129 | super(geometry, materials); 130 | 131 | this.idMatrix = SkinnedMesh.defaultMatrix(); 132 | this.bones = []; 133 | this.boneMatrices = []; 134 | //... 135 | } 136 | update(camera) { 137 | //... 138 | super.update(); 139 | } 140 | static defaultMatrix() { 141 | return new THREE.Matrix4(); 142 | } 143 | } 144 | ``` 145 | ```` 146 | 147 | __输出:__ 148 | 149 | ```js {highlight:[3,'5-7',12]} 150 | class SkinnedMesh extends THREE.Mesh { 151 | constructor(geometry, materials) { 152 | super(geometry, materials); 153 | 154 | this.idMatrix = SkinnedMesh.defaultMatrix(); 155 | this.bones = []; 156 | this.boneMatrices = []; 157 | //... 158 | } 159 | update(camera) { 160 | //... 161 | super.update(); 162 | } 163 | static defaultMatrix() { 164 | return new THREE.Matrix4(); 165 | } 166 | } 167 | ``` 168 | 169 | ## Adding Vue Mixin 170 | 171 | Adding a Vue mixin to the Markdown component: 172 | 173 | ````markdown 174 | <button @click="count++">{{ count }}</button> people love Docute. 175 | 176 | ```js {mixin:true} 177 | { 178 | data() { 179 | return { 180 | count: 1000 181 | } 182 | } 183 | } 184 | ``` 185 | ```` 186 | 187 | <button @click="count++">{{ count }}</button> people love Docute. 188 | 189 | ```js {mixin:true} 190 | { 191 | data() { 192 | return { 193 | count: 1000 194 | } 195 | } 196 | } 197 | ``` 198 | 199 | ## 使用 Mermaid 200 | 201 | [Mermaid](https://mermaidjs.github.io/) 是一种纯文本撰写图表的方法,你可以使用简单的 Docute 插件来添加对 Mermaid 的支持: 202 | 203 | ```html 204 | <script src="https://unpkg.com/docute@4/dist/docute.js"></script> 205 | <!-- Load mermaid --> 206 | <script src="https://unpkg.com/mermaid@8.0.0-rc.8/dist/mermaid.min.js"></script> 207 | <!-- Load the mermaid plugin --> 208 | <script src="https://unpkg.com/docute-mermaid@1/dist/index.min.js"></script> 209 | 210 | <!-- Use the plugin --> 211 | <script> 212 | new Docute({ 213 | // ... 214 | plugins: [ 215 | docuteMermaid() 216 | ] 217 | }) 218 | </script> 219 | ``` 220 | 221 | ## HTML in Markdown 222 | 223 | You can write HTML in Markdown, for example: 224 | 225 | ```markdown 226 | __FAQ__: 227 | 228 | <details><summary>what is the meaning of life?</summary> 229 | 230 | some say it is __42__, some say it is still unknown. 231 | </details> 232 | ``` 233 | 234 | __FAQ__: 235 | 236 | <details><summary>what is the meaning of life?</summary> 237 | 238 | some say it is __42__, some say it is still unknown. 239 | </details> 240 | 241 | <br> 242 | 243 | <Note>In fact you can even use Vue directives and Vue components in Markdown too, learn more about it [here](./use-vue-in-markdown.md).</Note> 244 | -------------------------------------------------------------------------------- /website/docs/zh/builtin-components.md: -------------------------------------------------------------------------------- 1 | # 内置组件 2 | 3 | Docute 附带一些内置的 Vue 组件。 4 | 5 | ## `<ImageZoom>` 6 | 7 | 使用与 Medium 相同的缩放效果显示 image 。 8 | 9 | |属性|类型|默认值|描述| 10 | |---|---|---|---| 11 | |url|`string`|N/A|Image 的 URL| 12 | | title | `string` | N/A | Image title | 13 | |alt|`string`|N/A|占位文字| 14 | |border|`boolean`|`false`|是否显示图像周围的边框| 15 | |width|`string`|N/A|Image 宽度| 16 | 17 | 示例: 18 | 19 | ```markdown 20 | <ImageZoom 21 | src="https://i.loli.net/2018/09/24/5ba8e878850e9.png" 22 | :border="true" 23 | width="300" 24 | /> 25 | ``` 26 | 27 | <ImageZoom src="https://i.loli.net/2018/09/24/5ba8e878850e9.png" :border="true" width="300"/> 28 | 29 | 30 | ## `<Badge>` 31 | 32 | A small count and labeling component. 33 | 34 | | Prop | Type | Default | Description | 35 | | -------- | --------------------------------------------------- | ------- | ----------------------- | 36 | | type | <code>'tip' | 'success' | 'warning' | 'danger'</code> | N/A | Badge type | 37 | | color | `string` | N/A | Custom background color | 38 | | children | `string` | N/A | Badge text | 39 | 40 | <br> 41 | 42 | Example: 43 | 44 | ```markdown 45 | - Feature 1 <Badge>Badge</Badge> 46 | - Feature 2 <Badge type="tip">Tip</Badge> 47 | - Feature 3 <Badge type="success">Success</Badge> 48 | - Feature 4 <Badge type="warning">Warning</Badge> 49 | - Feature 5 <Badge type="danger">Danger</Badge> 50 | - Feature 6 <Badge color="magenta">Custom Color</Badge> 51 | ``` 52 | 53 | - Feature 1 <Badge>Badge</Badge> 54 | - Feature 2 <Badge type="tip">Tip</Badge> 55 | - Feature 3 <Badge type="success">Success</Badge> 56 | - Feature 4 <Badge type="warning">Warning</Badge> 57 | - Feature 5 <Badge type="danger">Danger</Badge> 58 | - Feature 6 <Badge color="magenta">Custom Color</Badge> 59 | 60 | ## `<Note>` 61 | 62 | Colored note blocks, to emphasize part of your page. 63 | 64 | | Prop | Type | Default | Description | 65 | | -------- | ------------------------------------------------------------------- | ------------------- | ------------------------------------------------- | 66 | | type | <code>'tip' | 'warning' | 'danger' | 'success'</code> | N/A | Note type | 67 | | label | `string` `boolean` | The value of `type` | Custom note label text, use `false` to hide label | 68 | | children | `string` | N/A | Note content | 69 | 70 | <br> 71 | 72 | Examples: 73 | 74 | ```markdown 75 | <Note> 76 | 77 | This is a note that details something important.<br> 78 | [A link to helpful information.](https://docute.org) 79 | 80 | </Note> 81 | 82 | <!-- Tip Note --> 83 | <Note type="tip"> 84 | 85 | This is a tip for something that is possible. 86 | 87 | </Note> 88 | 89 | <!-- Warning Note --> 90 | <Note type="warning"> 91 | 92 | This is a warning for something very important. 93 | 94 | </Note> 95 | 96 | <!-- Danger Note --> 97 | <Note type="danger"> 98 | 99 | This is a danger for something to take action for. 100 | 101 | </Note> 102 | ``` 103 | 104 | <Note> 105 | 106 | This is a note that details something important.<br> 107 | [A link to helpful information.](https://docute.org) 108 | 109 | </Note> 110 | 111 | <!-- Tip Note --> 112 | <Note type="tip"> 113 | 114 | This is a tip for something that is possible. 115 | 116 | </Note> 117 | 118 | <!-- Warning Note --> 119 | <Note type="warning"> 120 | 121 | This is a warning for something very important. 122 | 123 | </Note> 124 | 125 | <!-- Danger Note --> 126 | <Note type="danger"> 127 | 128 | This is a danger for something to take action for. 129 | 130 | </Note> 131 | 132 | ## `<Gist>` 133 | 134 | Embed [GitHub Gist](https://gist.github.com/) into your Markdown documents. 135 | 136 | |Prop|Type|Default|Description| 137 | |---|---|---|---| 138 | |id|`string`|N/A|Gist ID| 139 | 140 | Example: 141 | 142 | ```markdown 143 | <Gist id="71b8002ecd62a68fa7d7ee52011b2c7c" /> 144 | ``` 145 | 146 | <Gist id="71b8002ecd62a68fa7d7ee52011b2c7c" /> 147 | 148 | ## `<docute-select>` 149 | 150 | A customized `<select>` component: 151 | 152 | <!-- prettier-ignore --> 153 | ````vue 154 | <docute-select :value="favoriteFruit" @change="handleChange"> 155 | <option value="apple" :selected="favoriteFruit === 'apple'">Apple</option> 156 | <option value="banana" :selected="favoriteFruit === 'banana'">Banana</option> 157 | <option value="watermelon" :selected="favoriteFruit === 'watermelon'">Watermelon</option> 158 | </docute-select> 159 | 160 | Your favorite fruit: {{ favoriteFruit }} 161 | 162 | ```js {mixin: true} 163 | module.exports = { 164 | data() { 165 | return { 166 | favoriteFruit: 'banana' 167 | } 168 | }, 169 | methods: { 170 | handleChange(value) { 171 | this.favoriteFruit = value 172 | } 173 | } 174 | } 175 | ``` 176 | ```` 177 | 178 | <docute-select @change="handleChange" :value="favoriteFruit"> 179 | <option value="apple" :selected="favoriteFruit === 'apple'">Apple</option> 180 | <option value="banana" :selected="favoriteFruit === 'banana'">Banana</option> 181 | <option value="watermelon" :selected="favoriteFruit === 'watermelon'">Watermelon</option> 182 | </docute-select> 183 | 184 | Your favorite fruit: {{ favoriteFruit }} 185 | 186 | ```js {mixin: true} 187 | { 188 | data() { 189 | return { 190 | favoriteFruit: 'banana' 191 | } 192 | }, 193 | methods: { 194 | handleChange(value) { 195 | this.favoriteFruit = value 196 | } 197 | } 198 | } 199 | ``` 200 | 201 | ## `<v-style>` `<v-script>` 202 | 203 | 在 Vue template 中替代 `<style>` 和 `<script>` 标签。 204 | 205 | 通常,在 Markdown 里你并不需要直接使用此组件。因为我们会自动将 markdown 中的 `<style>` 和 `<script>` 标签转换为该组件。 206 | -------------------------------------------------------------------------------- /website/docs/zh/options.md: -------------------------------------------------------------------------------- 1 | # 配置项 2 | 3 | 用于 `Docute` 构造函数的配置项。 4 | 5 | ```js 6 | new Docute(options) 7 | ``` 8 | 9 | 10 | ## target 11 | 12 | - Type: `string` 13 | - Default: `docute` 14 | 15 | 目标元素的 ID,会被用于创建 app,比如 `app` 或者 `#app`。 16 | 17 | ## title 18 | 19 | - 类型:`string` 20 | - 默认值:`document.title` 21 | 22 | 网站标题。 23 | 24 | ## logo 25 | 26 | - Type: `string` `object` 27 | - Default: `<span>{{ $store.getters.config.title }}</span>` 28 | 29 | Customize the logo in header. 30 | 31 | - `string`: Used as Vue template. 32 | - `object`: Used as Vue component. 33 | 34 | ## nav 35 | 36 | - 类型: `Array<NavItem>` 37 | 38 | 在头部显示的导航栏。 39 | 40 | ```ts 41 | interface NavItem { 42 | title: string 43 | link?: string 44 | // Whether to open the link in a new tab 45 | // Only works for external links 46 | // Defaults to `true` 47 | openInNewTab?: boolean 48 | // Use `children` instead of `link` to display dropdown 49 | children?: Array<NavItemLink> 50 | } 51 | 52 | interface NavItemLink { 53 | title: string 54 | link: string 55 | openInNewTab?: boolean 56 | } 57 | ``` 58 | 59 | ## sidebar 60 | 61 | - 类型:`Array<SidebarItem>` `(store: Vuex.Store) => Array<SidebarItem>` 62 | 63 | 在侧边栏中显示的导航栏。 64 | 65 | ```ts 66 | type SidebarItem = SingleItem | MultiItem 67 | 68 | interface SingleItem { 69 | title: string 70 | link: string 71 | // Whether to open the link in a new tab 72 | // Only works for external links 73 | // Defaults to `true` 74 | openInNewTab?: boolean 75 | } 76 | 77 | interface MultiItem { 78 | title: string 79 | children: Array<SingleItem> 80 | /** 81 | * Whether to show TOC 82 | * Default to `true` 83 | */ 84 | toc?: boolean 85 | /** 86 | * Whether to make children collapsable 87 | * Default to `true` 88 | */ 89 | collapsable?: boolean 90 | } 91 | ``` 92 | 93 | ## sourcePath 94 | 95 | - 类型:`string` 96 | - 默认值:`'./'` 97 | 98 | 从 source path 获取 markdown 文件,默认情况下,我们从 `index.html` 所在的目录加载它们。 99 | 100 | 它也可以是完整的 URL,例如: `https://some-website/path/to/markdown/files`,以便于你可以从其他域名加载文件。 101 | 102 | 103 | ## routes 104 | 105 | - Type: `Routes` 106 | 107 | Use this option to make Docute fetch specific file or use given content for a path. 108 | 109 | ```ts 110 | interface Routes { 111 | [path: string]: RouteData 112 | } 113 | 114 | interface RouteData { 115 | /* Default to the content h1 header */ 116 | title?: string 117 | /* One of `content` and `file` is required */ 118 | content?: string 119 | /* Response will be used as `content` */ 120 | file?: string 121 | /* Parse the content as markdown, true by default */ 122 | markdown?: boolean 123 | [k: string]?: any 124 | } 125 | ``` 126 | 127 | ## componentMixins 128 | 129 | - 类型: `Array<Object>` 130 | 131 | 一个包含 [Vue mixins](https://vuejs.org/v2/api/#mixins) 的数组,会被应用到所有的 Markdown 组件中。 132 | 133 | ## highlight 134 | 135 | - 类型:`Array<string>` 136 | 137 | 需要语法高亮的语言名称数组。查阅 [Prism.js](https://unpkg.com/prismjs/components/) 获取所有支持的语言名称。(不需要携带 `prism-` 前缀)。 138 | 139 | 例如:`highlight: ['typescript', 'go', 'graphql']`。 140 | 141 | ## editLinkBase 142 | 143 | - 类型:`string` 144 | 145 | *编辑链接*的 URL 基础路径。 146 | 147 | 例如,如果将 markdown 文件存储在 Github 仓库的 master 分支的 `docs` 文件夹中,那么该路径应该为: 148 | 149 | ``` 150 | https://github.com/USER/REPO/blob/master/docs 151 | ``` 152 | 153 | ## editLinkText 154 | 155 | - 类型:`string` 156 | - 默认值:`'Edit this page'` 157 | 158 | *编辑链接*的文字内容。 159 | 160 | # theme 161 | 162 | - 类型: `string` 163 | - 默认值: `default` 164 | - 内置项: `default` `dark` 165 | 166 | 网站主题。 167 | 168 | ## detectSystemDarkTheme 169 | 170 | - Type: `boolean` 171 | - Default: `undefined` 172 | 173 | In recent versions of macOS (Mojave) and Windows 10, users have been able to enable a system level dark mode. Set this option to `true` so that Docute will use the dark theme by default if your system has it enabled. 174 | 175 | ## darkThemeToggler 176 | 177 | - Type: `boolean` 178 | - Default: `undefined` 179 | 180 | 181 | 显示一个按钮用于切换夜间主题。 182 | 183 | ## layout 184 | 185 | - 类型: `string` 186 | - 默认值: `wide` 187 | - 可选项: `wide` `narrow` `left` 188 | 189 | 网站布局。 190 | 191 | ## versions 192 | 193 | - 类型: `Versions` 194 | 195 | 设置此项之后, Docute 会在侧边栏显示一个版本选择器。 196 | 197 | ```ts 198 | interface Versions { 199 | // 版本号, 比如 `v1` 200 | [version: string]: { 201 | // 指向相关文档的链接 202 | link: string 203 | } 204 | } 205 | ``` 206 | 207 | ## cssVariables 208 | 209 | - Type: `object` `(theme: string) => object` 210 | 211 | Override CSS variables. 212 | 213 | ## overrides 214 | 215 | - 类型:`{[path: string]: LocaleOptions}` 216 | 217 | ```ts 218 | interface LocaleOptions extends Options { 219 | language: string 220 | } 221 | ``` 222 | 223 | ## router 224 | 225 | - Type: `ConstructionOptions` 226 | 227 | All vue-router's [Construction options](https://router.vuejs.org/api/#router-construction-options) except for `routes` are accepted here. 228 | 229 | For example, you can set `router: { mode: 'history' }` to [get rid of the hash](https://router.vuejs.org/guide/essentials/history-mode.html#example-server-configurations) in URLs. 230 | 231 | ## banner / footer 232 | 233 | - Type: `string` `VueComponent` 234 | 235 | Display banner and footer. A string will be wrapped inside `<div class="docute-banner">` or `<div class="docute-footer">` and used as Vue template. 236 | 237 | For example: 238 | 239 | ```js 240 | new Docute({ 241 | banner: `Please <a href="https://donate.com/link"> 242 | donate</a> <ExternalLinkIcon /> to support this project!` 243 | }) 244 | ``` 245 | 246 | You can also use a Vue component: 247 | 248 | ```js 249 | new Docute({ 250 | banner: { 251 | template: ` 252 | <div class="docute-banner"> 253 | Please <a href="https://donate.com/link"> 254 | donate</a> <ExternalLinkIcon /> to support this project! 255 | </div> 256 | ` 257 | } 258 | }) 259 | ``` 260 | 261 | ## imageZoom 262 | 263 | - Type: `boolean` 264 | - Default: `undefined` 265 | 266 | Enable Medium-like image zoom effect to all images. 267 | 268 | Alternatively you can use the [`<image-zoom>`](./builtin-components.md#imagezoom) component if you only need this in specific images. 269 | 270 | ## fetchOptions 271 | 272 | - Type: `object` 273 | 274 | The option for [`window.fetch`](https://developer.mozilla.org/en-US/docs/Web/API/Fetch_API/Using_Fetch). 275 | -------------------------------------------------------------------------------- /website/docs/guide/markdown-features.md: -------------------------------------------------------------------------------- 1 | # Markdown Features 2 | 3 | A document should be easy-to-read and easy-to-write. 4 | 5 | ## Document Format 6 | 7 | A document is represented in Markdown format: 8 | 9 | ```markdown 10 | # Title 11 | 12 | The content goes here... 13 | ``` 14 | 15 | Internally we use the blazing-fast [marked](https://marked.js.org) to parse Markdown, so almost all [GitHub Flavored Markdown](https://github.github.com/gfm/) features are supported. 16 | 17 | Check out the introduction for [Markdown](https://daringfireball.net/projects/markdown/) if you're not sure what it is. 18 | 19 | ## Links 20 | 21 | ### Internal Links 22 | 23 | Internal links are converted to `<router-link>` for SPA-style navigation. 24 | 25 | __Input__: 26 | 27 | ```markdown 28 | - [Home](/) <!-- Send the user to Homepage --> 29 | - [Use Vue in Markdown](/guide/use-vue-in-markdown) <!-- Send the user to another page --> 30 | - [Check out the `title` option](../options.md#title) <!-- Even relative link to markdown tile --> 31 | ``` 32 | 33 | __Output__: 34 | 35 | - [Home](/) <!-- Send the user to Homepage --> 36 | - [Use Vue in Markdown](/guide/use-vue-in-markdown) <!-- Send the user to another page --> 37 | - [Check out the `title` option](../options.md#title) <!-- Even relative link to markdown tile --> 38 | 39 | 40 | ### External Links 41 | 42 | External links automatically gets HTML attributes `target="_blank" rel="noopener noreferrer"`, for example: 43 | 44 | __Input__: 45 | 46 | ```markdown 47 | - [Docute website](https://docute.org) 48 | - [Docute repo](https://github.com/egoist/docute) 49 | ``` 50 | 51 | __Output__: 52 | 53 | - [Docute website](https://docute.org) 54 | - [Docute repo](https://github.com/egoist/docute) 55 | 56 | ## Task List 57 | 58 | __Input__: 59 | 60 | ```markdown 61 | - [ ] Rule the web 62 | - [x] Conquer the world 63 | - [ ] Learn Docute 64 | ``` 65 | 66 | __Output__: 67 | 68 | - [ ] Rule the web 69 | - [x] Conquer the world 70 | - [ ] Learn Docute 71 | 72 | ## Code Highlighting 73 | 74 | Code fences will be highlighted using [Prism.js](https://prismjs.com/), example code: 75 | 76 | ```js 77 | // Returns a function, that, as long as it continues to be invoked, will not 78 | // be triggered. The function will be called after it stops being called for 79 | // N milliseconds. If `immediate` is passed, trigger the function on the 80 | // leading edge, instead of the trailing. 81 | function debounce(func, wait, immediate) { 82 | var timeout; 83 | return function() { 84 | var context = this, args = arguments; 85 | var later = function() { 86 | timeout = null; 87 | if (!immediate) func.apply(context, args); 88 | }; 89 | var callNow = immediate && !timeout; 90 | clearTimeout(timeout); 91 | timeout = setTimeout(later, wait); 92 | if (callNow) func.apply(context, args); 93 | }; 94 | }; 95 | ``` 96 | 97 | The languages we support by default: 98 | 99 | <ul> 100 | <li v-for="lang in builtinLanguages" :key="lang"> 101 | {{ lang }} 102 | </li> 103 | </ul> 104 | 105 | You can use [highlight](/options#highlight) option to add more languages. 106 | 107 | ## Code Fence Options 108 | 109 | Next to the code fence language, you can use a JS object to specify options: 110 | 111 | ````markdown 112 | ```js {highlightLines: [2]} 113 | function foo() { 114 | console.log('foo') 115 | } 116 | ``` 117 | ```` 118 | 119 | Available options: 120 | 121 | - `highlightLines`: [Line Highlighting in Code Fences](#line-highlighting-in-code-fences) 122 | - `mixin`: [Adding Vue Mixin](#adding-vue-mixin) 123 | 124 | ## Line Highlighting in Code Fences 125 | 126 | __Input:__ 127 | 128 | ````markdown 129 | ```js {highlight:[3,'5-7',12]} 130 | class SkinnedMesh extends THREE.Mesh { 131 | constructor(geometry, materials) { 132 | super(geometry, materials); 133 | 134 | this.idMatrix = SkinnedMesh.defaultMatrix(); 135 | this.bones = []; 136 | this.boneMatrices = []; 137 | //... 138 | } 139 | update(camera) { 140 | //... 141 | super.update(); 142 | } 143 | static defaultMatrix() { 144 | return new THREE.Matrix4(); 145 | } 146 | } 147 | ``` 148 | ```` 149 | 150 | __Output:__ 151 | 152 | ```js {highlight:[3,'5-7',12]} 153 | class SkinnedMesh extends THREE.Mesh { 154 | constructor(geometry, materials) { 155 | super(geometry, materials); 156 | 157 | this.idMatrix = SkinnedMesh.defaultMatrix(); 158 | this.bones = []; 159 | this.boneMatrices = []; 160 | //... 161 | } 162 | update(camera) { 163 | //... 164 | super.update(); 165 | } 166 | static defaultMatrix() { 167 | return new THREE.Matrix4(); 168 | } 169 | } 170 | ``` 171 | 172 | ## Adding Vue Mixin 173 | 174 | Adding a Vue mixin to the Markdown component: 175 | 176 | ````markdown 177 | <button @click="count++">{{ count }}</button> people love Docute. 178 | 179 | ```js {mixin:true} 180 | { 181 | data() { 182 | return { 183 | count: 1000 184 | } 185 | } 186 | } 187 | ``` 188 | ```` 189 | 190 | <button @click="count++">{{ count }}</button> people love Docute. 191 | 192 | ```js {mixin:true} 193 | { 194 | data() { 195 | return { 196 | count: 1000 197 | } 198 | } 199 | } 200 | ``` 201 | 202 | ## Using Mermaid 203 | 204 | [Mermaid](https://mermaidjs.github.io/) is a way to write charts in plain text, you can use a simple Docute plugin to add Mermaid support: 205 | 206 | ```html 207 | <script src="https://unpkg.com/docute@4/dist/docute.js"></script> 208 | <!-- Load mermaid --> 209 | <script src="https://unpkg.com/mermaid@8.0.0-rc.8/dist/mermaid.min.js"></script> 210 | <!-- Load the mermaid plugin --> 211 | <script src="https://unpkg.com/docute-mermaid@1/dist/index.min.js"></script> 212 | 213 | <!-- Use the plugin --> 214 | <script> 215 | new Docute({ 216 | // ... 217 | plugins: [ 218 | docuteMermaid() 219 | ] 220 | }) 221 | </script> 222 | ``` 223 | 224 | ## HTML in Markdown 225 | 226 | You can write HTML in Markdown, for example: 227 | 228 | ```markdown 229 | __FAQ__: 230 | 231 | <details><summary>what is the meaning of life?</summary> 232 | 233 | some say it is __42__, some say it is still unknown. 234 | </details> 235 | ``` 236 | 237 | __FAQ__: 238 | 239 | <details><summary>what is the meaning of life?</summary> 240 | 241 | some say it is __42__, some say it is still unknown. 242 | </details> 243 | 244 | <br> 245 | 246 | <Note>In fact you can even use Vue directives and Vue components in Markdown too, learn more about it [here](./use-vue-in-markdown.md).</Note> 247 | -------------------------------------------------------------------------------- /website/docs/builtin-components.md: -------------------------------------------------------------------------------- 1 | # Built-in Components 2 | 3 | Docute comes with a set of built-in Vue components. 4 | 5 | ## `<ImageZoom>` 6 | 7 | Use medium-style zoom effect to display certain image. 8 | 9 | | Prop | Type | Default | Description | 10 | | ------ | --------- | ------- | ------------------------ | 11 | | src | `string` | N/A | URL to image | 12 | | title | `string` | N/A | Image title | 13 | | alt | `string` | N/A | Placeholder text | 14 | | border | `boolean` | `false` | Show border around image | 15 | | width | `string` | N/A | Image width | 16 | 17 | <br> 18 | 19 | Example: 20 | 21 | ```markdown 22 | <ImageZoom 23 | src="https://i.loli.net/2018/09/24/5ba8e878850e9.png" 24 | :border="true" 25 | width="300" 26 | /> 27 | ``` 28 | 29 | <ImageZoom src="https://i.loli.net/2018/09/24/5ba8e878850e9.png" :border="true" width="300"/> 30 | 31 | ## `<Badge>` 32 | 33 | A small count and labeling component. 34 | 35 | | Prop | Type | Default | Description | 36 | | -------- | -------------------------------------------------------------------- | ------- | ----------------------- | 37 | | type | <code>'tip' | 'success' | 'warning' | 'danger'</code> | N/A | Badge type | 38 | | color | `string` | N/A | Custom background color | 39 | | children | `string` | N/A | Badge text | 40 | 41 | <br> 42 | 43 | Example: 44 | 45 | ```markdown 46 | - Feature 1 <Badge>Badge</Badge> 47 | - Feature 2 <Badge type="tip">Tip</Badge> 48 | - Feature 3 <Badge type="success">Success</Badge> 49 | - Feature 4 <Badge type="warning">Warning</Badge> 50 | - Feature 5 <Badge type="danger">Danger</Badge> 51 | - Feature 6 <Badge color="magenta">Custom Color</Badge> 52 | ``` 53 | 54 | - Feature 1 <Badge>Badge</Badge> 55 | - Feature 2 <Badge type="tip">Tip</Badge> 56 | - Feature 3 <Badge type="success">Success</Badge> 57 | - Feature 4 <Badge type="warning">Warning</Badge> 58 | - Feature 5 <Badge type="danger">Danger</Badge> 59 | - Feature 6 <Badge color="magenta">Custom Color</Badge> 60 | 61 | ## `<Note>` 62 | 63 | Colored note blocks, to emphasize part of your page. 64 | 65 | | Prop | Type | Default | Description | 66 | | -------- | -------------------------------------------------------------------- | ------------------- | ------------------------------------------------- | 67 | | type | <code>'tip' | 'warning' | 'danger' | 'success'</code> | N/A | Note type | 68 | | label | `string` `boolean` | The value of `type` | Custom note label text, use `false` to hide label | 69 | | children | `string` | N/A | Note content | 70 | 71 | <br> 72 | 73 | Examples: 74 | 75 | ```markdown 76 | <Note> 77 | 78 | This is a note that details something important.<br> 79 | [A link to helpful information.](https://docute.org) 80 | 81 | </Note> 82 | 83 | <!-- Tip Note --> 84 | <Note type="tip"> 85 | 86 | This is a tip for something that is possible. 87 | 88 | </Note> 89 | 90 | <!-- Warning Note --> 91 | <Note type="warning"> 92 | 93 | This is a warning for something very important. 94 | 95 | </Note> 96 | 97 | <!-- Danger Note --> 98 | <Note type="danger"> 99 | 100 | This is a danger for something to take action for. 101 | 102 | </Note> 103 | ``` 104 | 105 | <Note> 106 | 107 | This is a note that details something important.<br> 108 | [A link to helpful information.](https://docute.org) 109 | 110 | </Note> 111 | 112 | <!-- Tip Note --> 113 | <Note type="tip"> 114 | 115 | This is a tip for something that is possible. 116 | 117 | </Note> 118 | 119 | <!-- Warning Note --> 120 | <Note type="warning"> 121 | 122 | This is a warning for something very important. 123 | 124 | </Note> 125 | 126 | <!-- Danger Note --> 127 | <Note type="danger"> 128 | 129 | This is a danger for something to take action for. 130 | 131 | </Note> 132 | 133 | ## `<Gist>` 134 | 135 | Embed [GitHub Gist](https://gist.github.com/) into your Markdown documents. 136 | 137 | | Prop | Type | Default | Description | 138 | | ---- | -------- | ------- | ----------- | 139 | | id | `string` | N/A | Gist ID | 140 | 141 | Example: 142 | 143 | ```markdown 144 | <Gist id="71b8002ecd62a68fa7d7ee52011b2c7c" /> 145 | ``` 146 | 147 | <Gist id="71b8002ecd62a68fa7d7ee52011b2c7c" /> 148 | 149 | ## `<docute-select>` 150 | 151 | A customized `<select>` component: 152 | 153 | <!-- prettier-ignore --> 154 | ````vue 155 | <docute-select :value="favoriteFruit" @change="handleChange"> 156 | <option value="apple" :selected="favoriteFruit === 'apple'">Apple</option> 157 | <option value="banana" :selected="favoriteFruit === 'banana'">Banana</option> 158 | <option value="watermelon" :selected="favoriteFruit === 'watermelon'">Watermelon</option> 159 | </docute-select> 160 | 161 | Your favorite fruit: {{ favoriteFruit }} 162 | 163 | ```js {mixin: true} 164 | module.exports = { 165 | data() { 166 | return { 167 | favoriteFruit: 'banana' 168 | } 169 | }, 170 | methods: { 171 | handleChange(value) { 172 | this.favoriteFruit = value 173 | } 174 | } 175 | } 176 | ``` 177 | ```` 178 | 179 | <docute-select @change="handleChange" :value="favoriteFruit"> 180 | <option value="apple" :selected="favoriteFruit === 'apple'">Apple</option> 181 | <option value="banana" :selected="favoriteFruit === 'banana'">Banana</option> 182 | <option value="watermelon" :selected="favoriteFruit === 'watermelon'">Watermelon</option> 183 | </docute-select> 184 | 185 | Your favorite fruit: {{ favoriteFruit }} 186 | 187 | ```js {mixin: true} 188 | { 189 | data() { 190 | return { 191 | favoriteFruit: 'banana' 192 | } 193 | }, 194 | methods: { 195 | handleChange(value) { 196 | this.favoriteFruit = value 197 | } 198 | } 199 | } 200 | ``` 201 | 202 | ## `<v-style>` `<v-script>` 203 | 204 | A hack for using `<style>` and `<script>` tags Vue template. 205 | 206 | In general you don't need to use them directly, since we automatically convert `<style>` and `<script>` tags in Markdown to these components. 207 | -------------------------------------------------------------------------------- /src/views/Home.vue: -------------------------------------------------------------------------------- 1 | <template> 2 | <div class="Page" :class="{[`layout-${$store.getters.config.layout}`]: true}"> 3 | <SiteHeader /> 4 | <div class="Wrap"> 5 | <Sidebar /> 6 | <SidebarMask /> 7 | <div class="Main"> 8 | <div class="Content" v-if="$store.state.fetchingFile"> 9 | <content-loader 10 | :height="160" 11 | :width="400" 12 | :speed="2" 13 | :primaryColor="$store.getters.cssVariables.loaderPrimaryColor" 14 | :secondaryColor="$store.getters.cssVariables.loaderSecondaryColor" 15 | > 16 | <rect x="0" y="5" rx="4" ry="4" width="117" height="6.4" /> 17 | <rect x="0" y="25" rx="3" ry="3" width="85" height="6.4" /> 18 | <rect x="0" y="60" rx="3" ry="3" width="350" height="6.4" /> 19 | <rect x="0" y="80" rx="3" ry="3" width="380" height="6.4" /> 20 | <rect x="0" y="100" rx="3" ry="3" width="201" height="6.4" /> 21 | </content-loader> 22 | </div> 23 | <div class="Content" v-else> 24 | <InjectedComponents position="content:start" /> 25 | <component v-if="pageTitle" :is="MarkdownTitle" class="page-title" /> 26 | <component :class="{'has-page-title': pageTitle}" :is="PageContent" /> 27 | <EditLink /> 28 | <PrevNextLinks /> 29 | <InjectedComponents position="content:end" /> 30 | </div> 31 | </div> 32 | </div> 33 | </div> 34 | </template> 35 | 36 | <script> 37 | import Vue from 'vue' 38 | import jump from 'jump.js' 39 | import {ContentLoader} from 'vue-content-loader' 40 | import Sidebar from '../components/Sidebar.vue' 41 | import SidebarMask from '../components/SidebarMask.vue' 42 | import SiteHeader from '../components/Header.vue' 43 | import PrevNextLinks from '../components/PrevNextLinks.vue' 44 | import EditLink from '../components/EditLink.vue' 45 | import {INITIAL_STATE_NAME} from '../utils/constants' 46 | import hooks from '../hooks' 47 | 48 | export default { 49 | name: 'PageHome', 50 | 51 | components: { 52 | ContentLoader, 53 | Sidebar, 54 | SidebarMask, 55 | SiteHeader, 56 | PrevNextLinks, 57 | EditLink 58 | }, 59 | 60 | async serverPrefetch() { 61 | await this.fetchFile(this.$route.path) 62 | this.setTitle() 63 | }, 64 | 65 | mounted() { 66 | if (!window[INITIAL_STATE_NAME]) { 67 | this.fetchFile(this.$route.path).then(this.setInitialState) 68 | } 69 | }, 70 | 71 | beforeRouteUpdate(to, from, next) { 72 | next() 73 | if (to.path !== from.path) { 74 | this.fetchFile(to.path) 75 | } 76 | }, 77 | 78 | watch: { 79 | '$route.hash'() { 80 | this.$nextTick(() => { 81 | this.jumpToHash() 82 | }) 83 | }, 84 | 85 | pageTitle() { 86 | this.setTitle() 87 | } 88 | }, 89 | 90 | computed: { 91 | pageTitle() { 92 | return this.$store.state.page.title 93 | }, 94 | 95 | MarkdownTitle() { 96 | return { 97 | name: 'MarkdownTitle', 98 | template: `<h1>${this.pageTitle}</h1>` 99 | } 100 | }, 101 | 102 | PageContent() { 103 | const {env} = this.$store.state 104 | const {componentMixins = []} = this.$store.getters.config 105 | const component = { 106 | mixins: [ 107 | ...componentMixins, 108 | ...env.mixins.map(mixin => { 109 | // eslint-disable-next-line no-new-func 110 | const fn = new Function('Vue', `return ${mixin.trim()}`) 111 | return fn(Vue) 112 | }) 113 | ], 114 | name: 'PageContent', 115 | template: `<div class="page-content">${this.$store.state.page.content}</div>` 116 | } 117 | 118 | hooks.process('extendMarkdownComponent', component) 119 | 120 | return component 121 | } 122 | }, 123 | 124 | methods: { 125 | async fetchFile(path) { 126 | await this.$store.dispatch('fetchFile', path) 127 | hooks.invoke('onContentWillUpdate', this) 128 | await this.$nextTick() 129 | hooks.invoke('onContentUpdated', this) 130 | this.jumpToHash() 131 | }, 132 | 133 | jumpToHash() { 134 | const hash = decodeURI(this.$route.hash) 135 | if (hash) { 136 | const el = document.querySelector(hash) 137 | if (el) { 138 | const header = document.querySelector('.Header') 139 | jump(el, { 140 | a11y: true, 141 | duration: 0, 142 | offset: -(header.clientHeight + 30) 143 | }) 144 | } 145 | } 146 | }, 147 | 148 | setInitialState() { 149 | if (/(Prerender|jsdom|PhantomJS)/i.test(navigator.userAgent)) { 150 | const script = document.createElement('script') 151 | script.textContent = `window.${INITIAL_STATE_NAME} = ${JSON.stringify({ 152 | page: this.$store.state.page, 153 | env: this.$store.state.env, 154 | fetchingFile: false 155 | })}` 156 | document.head.appendChild(script) 157 | } 158 | }, 159 | 160 | setTitle() { 161 | const {path} = this.$route 162 | const {config, homePaths} = this.$store.getters 163 | 164 | let title = 165 | homePaths.indexOf(path) > -1 166 | ? config.title 167 | : `${this.pageTitle} - ${config.title}` 168 | 169 | // Strip HTML tags 170 | title = title.replace(/<(?:.|\n)*?>/gm, '') 171 | if (this.$ssrContext) { 172 | this.$ssrContext.title = title 173 | } else { 174 | document.title = title 175 | } 176 | } 177 | } 178 | } 179 | </script> 180 | 181 | <style src="../css/prism.css"></style> 182 | <style src="../css/page-content.css"></style> 183 | 184 | <style scoped> 185 | .Main { 186 | padding-left: var(--sidebar-width); 187 | padding-top: calc(var(--header-height) + 40px); 188 | padding-bottom: 2rem; 189 | background: var(--main-background); 190 | 191 | @media screen and (max-width: 768px) { 192 | padding-left: 0; 193 | } 194 | 195 | @media print { 196 | padding-left: 0; 197 | padding-top: 30px; 198 | } 199 | } 200 | 201 | .Content { 202 | padding: 0 20px 0 80px; 203 | 204 | @media screen and (max-width: 768px) { 205 | padding: 0 20px; 206 | } 207 | } 208 | 209 | .layout-wide .Content { 210 | max-width: 770px; 211 | margin: 0 auto; 212 | padding: 0 2.5rem; 213 | 214 | @media screen and (max-width: 768px) { 215 | max-width: 100%; 216 | padding: 0 20px; 217 | } 218 | 219 | @media print { 220 | padding: 0; 221 | } 222 | } 223 | 224 | .page-title { 225 | font-size: 3rem; 226 | margin: 0; 227 | margin-bottom: 1.4rem; 228 | font-weight: 300; 229 | line-height: 1.1; 230 | } 231 | </style> 232 | -------------------------------------------------------------------------------- /website/docs/options.md: -------------------------------------------------------------------------------- 1 | # Options 2 | 3 | The options used by `Docute` constructor. 4 | 5 | ```js 6 | new Docute(options) 7 | ``` 8 | 9 | ## target 10 | 11 | - Type: `string` 12 | - Default: `docute` 13 | 14 | The ID of the target element to locate, e.g. `app` or `#app`. 15 | 16 | ## title 17 | 18 | - Type: `string` 19 | - Default: `document.title` 20 | 21 | Website title. 22 | 23 | ## logo 24 | 25 | - Type: `string` `object` 26 | - Default: `<span>{{ $store.getters.config.title }}</span>` 27 | 28 | Customize the logo in header. 29 | 30 | - `string`: Used as Vue template. 31 | - `object`: Used as Vue component. 32 | 33 | ## nav 34 | 35 | - Type: `Array<NavItem>` 36 | 37 | An array of navigation items to display at navbar. 38 | 39 | ```ts 40 | interface NavItem { 41 | title: string 42 | link?: string 43 | // Whether to open the link in a new tab 44 | // Only works for external links 45 | // Defaults to `true` 46 | openInNewTab?: boolean 47 | // Or use `children` to display dropdown menu 48 | children?: Array<NavItemLink> 49 | } 50 | 51 | interface NavItemLink { 52 | title: string 53 | link: string 54 | openInNewTab?: boolean 55 | } 56 | ``` 57 | 58 | ## sidebar 59 | 60 | - Type: `Array<SidebarItem>` `(store: Vuex.Store) => Array<SidebarItem>` 61 | 62 | An array of navigation items to display at sidebar. 63 | 64 | ```ts 65 | type SidebarItem = SingleItem | MultiItem 66 | 67 | interface SingleItem { 68 | title: string 69 | link: string 70 | // Whether to open the link in a new tab 71 | // Only works for external links 72 | // Defaults to `true` 73 | openInNewTab?: boolean 74 | } 75 | 76 | interface MultiItem { 77 | title: string 78 | children: Array<SingleItem> 79 | /** 80 | * Whether to show TOC 81 | * Default to `true` 82 | */ 83 | toc?: boolean 84 | /** 85 | * Whether to make children collapsable 86 | * Default to `true` 87 | */ 88 | collapsable?: boolean 89 | } 90 | ``` 91 | 92 | ## sourcePath 93 | 94 | - Type: `string` 95 | - Default: `'./'` 96 | 97 | The source path to fetch markdown files from, by default we load them from path where your `index.html` is populated. 98 | 99 | It can also be a full URL like: `https://some-website/path/to/markdown/files` so that you can load files from a different domain. 100 | 101 | ## routes 102 | 103 | - Type: `Routes` 104 | 105 | Use this option to make Docute fetch specific file or use given content for a path. 106 | 107 | ```ts 108 | interface Routes { 109 | [path: string]: RouteData 110 | } 111 | 112 | interface RouteData { 113 | /* Default to the content h1 header */ 114 | title?: string 115 | /* One of `content` and `file` is required */ 116 | content?: string 117 | /* Response will be used as `content` */ 118 | file?: string 119 | /* Parse the content as markdown, true by default */ 120 | markdown?: boolean 121 | [k: string]?: any 122 | } 123 | ``` 124 | 125 | ## componentMixins 126 | 127 | - Type: `Array<Object>` 128 | 129 | Basically an array of [Vue mixins](https://vuejs.org/v2/api/#mixins) that are applied to all markdown components. 130 | 131 | 132 | ## highlight 133 | 134 | - Type: `Array<string>` 135 | 136 | An array of language names to highlight. Check out [Prism.js](https://unpkg.com/prismjs/components/) for all supported language names (without the `prism-` prefix). 137 | 138 | For example: `highlight: ['typescript', 'go', 'graphql']`. 139 | 140 | ## editLinkBase 141 | 142 | - Type: `string` 143 | 144 | The base path for the URL of *edit link*. 145 | 146 | e.g. If you store the markdown files in `docs` folder on master branch in a GitHub repo, then it should be: 147 | 148 | ``` 149 | https://github.com/USER/REPO/blob/master/docs 150 | ``` 151 | 152 | ## editLinkText 153 | 154 | - Type: `string` 155 | - Default: `'Edit this page'` 156 | 157 | The text for *edit link*. 158 | 159 | ## theme 160 | 161 | - Type: `string` 162 | - Default: `default` 163 | - Values: `default` `dark` 164 | 165 | Site theme. 166 | 167 | ## detectSystemDarkTheme 168 | 169 | - Type: `boolean` 170 | - Default: `undefined` 171 | 172 | In recent versions of macOS (Mojave) and Windows 10, users have been able to enable a system level dark mode. Set this option to `true` so that Docute will use the dark theme by default if your system has it enabled. 173 | 174 | ## darkThemeToggler 175 | 176 | - Type: `boolean` 177 | - Default: `undefined` 178 | 179 | 180 | Show a toggler for switching dark theme on and off. 181 | 182 | ## layout 183 | 184 | - Type: `string` 185 | - Default: `wide` 186 | - Values: `wide` `narrow` `left` 187 | 188 | Site layout. 189 | 190 | ## versions 191 | 192 | - Type: `Versions` 193 | 194 | When specified, Docute will show a version selector at the sidebar. 195 | 196 | ```ts 197 | interface Versions { 198 | // The version number, like `v1` 199 | [version: string]: { 200 | // The link to this version of docs 201 | link: string 202 | } 203 | } 204 | ``` 205 | 206 | ## cssVariables 207 | 208 | - Type: `object` `(theme: string) => object` 209 | 210 | Override CSS variables. 211 | 212 | ## overrides 213 | 214 | - Type: `{[path: string]: LocaleOptions}` 215 | 216 | ```ts 217 | interface LocaleOptions extends Options { 218 | language: string 219 | } 220 | ``` 221 | 222 | ## router 223 | 224 | - Type: `ConstructionOptions` 225 | 226 | All vue-router's [Construction options](https://router.vuejs.org/api/#router-construction-options) except for `routes` are accepted here. 227 | 228 | For example, you can set `router: { mode: 'history' }` to [get rid of the hash](https://router.vuejs.org/guide/essentials/history-mode.html#example-server-configurations) in URLs. 229 | 230 | 231 | ## banner / footer 232 | 233 | - Type: `string` `VueComponent` 234 | 235 | Display banner and footer. A string will be wrapped inside `<div class="docute-banner">` or `<div class="docute-footer">` and used as Vue template. 236 | 237 | For example: 238 | 239 | ```js 240 | new Docute({ 241 | banner: `Please <a href="https://donate.com/link"> 242 | donate</a> <ExternalLinkIcon /> to support this project!` 243 | }) 244 | ``` 245 | 246 | You can also use a Vue component: 247 | 248 | ```js 249 | new Docute({ 250 | banner: { 251 | template: ` 252 | <div class="docute-banner"> 253 | Please <a href="https://donate.com/link"> 254 | donate</a> <ExternalLinkIcon /> to support this project! 255 | </div> 256 | ` 257 | } 258 | }) 259 | ``` 260 | 261 | ## imageZoom 262 | 263 | - Type: `boolean` 264 | - Default: `undefined` 265 | 266 | Enable Medium-like image zoom effect to all images. 267 | 268 | Alternatively you can use the [`<image-zoom>`](./builtin-components.md#imagezoom) component if you only need this in specific images. 269 | 270 | ## fetchOptions 271 | 272 | - Type: `object` 273 | 274 | The option for [`window.fetch`](https://developer.mozilla.org/en-US/docs/Web/API/Fetch_API/Using_Fetch). 275 | -------------------------------------------------------------------------------- /src/store.js: -------------------------------------------------------------------------------- 1 | /* globals __PRISM_VERSION__ */ 2 | import Vue from 'vue' 3 | import Vuex from 'vuex' 4 | import marked from './utils/marked' 5 | import highlight from './utils/highlight' 6 | import {getFilenameByPath, getFileUrl, isExternalLink, inBrowser} from './utils' 7 | import markedRenderer from './utils/markedRenderer' 8 | import hooks from './hooks' 9 | import load from './utils/load' 10 | import prismLanguages from './utils/prismLanguages' 11 | import {defaultCssVariables, darkCssVariables} from './utils/cssVariables' 12 | import {INITIAL_STATE_NAME} from './utils/constants' 13 | 14 | Vue.use(Vuex) 15 | 16 | const initialState = inBrowser && window[INITIAL_STATE_NAME] 17 | 18 | const getDefaultTheme = (store, {theme, detectSystemDarkTheme}) => { 19 | if (!inBrowser || !detectSystemDarkTheme) { 20 | return theme || 'default' 21 | } 22 | 23 | const mq = window.matchMedia('(prefers-color-scheme: dark)') 24 | 25 | mq.addListener(() => { 26 | store.commit('SET_THEME', mq.matches ? 'dark' : 'default') 27 | }) 28 | 29 | return theme || (mq.matches ? 'dark' : 'default') 30 | } 31 | 32 | const store = new Vuex.Store({ 33 | state: { 34 | originalConfig: {}, 35 | page: { 36 | title: null, 37 | headings: null, 38 | html: '' 39 | }, 40 | env: {}, 41 | showSidebar: false, 42 | fetchingFile: true, 43 | ...initialState 44 | }, 45 | 46 | mutations: { 47 | SET_CONFIG(state, config = {}) { 48 | config.layout = config.layout || 'narrow' 49 | // TODO: remove `centerContent` in next major version 50 | if (config.centerContent) { 51 | config.layout = 'narrow' 52 | } 53 | config.theme = getDefaultTheme(store, config) 54 | state.originalConfig = config 55 | }, 56 | 57 | SET_PAGE(state, page) { 58 | state.page = page 59 | }, 60 | 61 | TOGGLE_SIDEBAR(state, show) { 62 | state.showSidebar = typeof show === 'boolean' ? show : !state.showSidebar 63 | }, 64 | 65 | SET_FETCHING(state, fetching) { 66 | state.fetchingFile = fetching 67 | }, 68 | 69 | SET_ENV(state, env) { 70 | state.env = env 71 | }, 72 | 73 | SET_THEME(state, theme) { 74 | state.originalConfig.theme = theme 75 | } 76 | }, 77 | 78 | actions: { 79 | async fetchFile({commit, getters, dispatch}, path) { 80 | commit('TOGGLE_SIDEBAR', false) 81 | commit('SET_FETCHING', true) 82 | 83 | let page = { 84 | markdown: true, 85 | ...(getters.config.routes && getters.config.routes[path]) 86 | } 87 | 88 | if (!page.content && !page.file) { 89 | const filename = getFilenameByPath(path) 90 | page.file = getFileUrl(getters.config.sourcePath, filename) 91 | page.editLink = 92 | getters.config.editLinkBase && 93 | getFileUrl(getters.config.editLinkBase, filename) 94 | } 95 | 96 | await Promise.all([ 97 | !page.content && 98 | fetch(page.file, getters.config.fetchOptions) 99 | .then(res => res.text()) 100 | .then(res => { 101 | page.content = res 102 | }), 103 | dispatch('fetchPrismLanguages') 104 | ]) 105 | 106 | page.content = await hooks.processPromise('processMarkdown', page.content) 107 | page = await hooks.processPromise('processPage', page) 108 | 109 | const env = { 110 | headings: [], 111 | mixins: [], 112 | config: getters.config 113 | } 114 | if (page.markdown) { 115 | page.content = marked(page.content, { 116 | renderer: markedRenderer(hooks), 117 | highlight, 118 | env 119 | }) 120 | } 121 | page.content = await hooks.processPromise('processHTML', page.content) 122 | page.headings = env.headings 123 | if (!page.title) { 124 | page.title = env.title 125 | } 126 | commit('SET_PAGE', page) 127 | commit('SET_ENV', env) 128 | commit('SET_FETCHING', false) 129 | }, 130 | 131 | fetchPrismLanguages({getters}) { 132 | const langs = getters.config.highlight 133 | 134 | if (!langs || langs.length === 0) { 135 | return Promise.resolve() 136 | } 137 | 138 | return load( 139 | langs 140 | .reduce((res, lang) => { 141 | if (prismLanguages[lang]) { 142 | res = res.concat(prismLanguages[lang]) 143 | } 144 | res.push(lang) 145 | return res 146 | }, []) 147 | .filter((lang, i, arr) => { 148 | // Dedupe 149 | return ( 150 | arr.indexOf(lang) === i && 151 | prismLanguages.builtin.indexOf(lang) === -1 152 | ) 153 | }) 154 | .map(lang => { 155 | return `https://unpkg.com/prismjs@${__PRISM_VERSION__}/components/prism-${lang}.js` 156 | }), 157 | 'prism-languages' 158 | ) 159 | } 160 | }, 161 | 162 | getters: { 163 | target({originalConfig: {target}}) { 164 | if (!target) return 'docute' 165 | if (target[0] === '#') return target.slice(1) 166 | return target 167 | }, 168 | 169 | languageOverrides({originalConfig}) { 170 | // `locales` is for legacy support 171 | const overrides = originalConfig.overrides || originalConfig.locales 172 | return ( 173 | overrides && 174 | Object.keys(overrides).reduce((res, path) => { 175 | if (overrides[path].language) { 176 | res[path] = overrides[path] 177 | } 178 | return res 179 | }, {}) 180 | ) 181 | }, 182 | 183 | currentLocalePath({route}, {languageOverrides}) { 184 | if (languageOverrides) { 185 | // Is it a locale? 186 | for (const localePath of Object.keys(languageOverrides)) { 187 | if (localePath !== '/') { 188 | const RE = new RegExp(`^${localePath}`) 189 | if (RE.test(route.path)) { 190 | return localePath 191 | } 192 | } 193 | } 194 | } 195 | 196 | return '/' 197 | }, 198 | 199 | config({originalConfig}, {currentLocalePath, languageOverrides}) { 200 | return languageOverrides 201 | ? { 202 | ...originalConfig, 203 | ...languageOverrides[currentLocalePath] 204 | } 205 | : originalConfig 206 | }, 207 | 208 | homePaths(_, {languageOverrides}) { 209 | const localePaths = languageOverrides 210 | ? Object.keys(languageOverrides) 211 | : [] 212 | return [...localePaths, '/'] 213 | }, 214 | 215 | sidebarLinks(_, {sidebar}) { 216 | return sidebar 217 | ? sidebar 218 | .reduce((res, next) => { 219 | // backward compabillity 220 | const children = next.children || next.links || [] 221 | return [...res, ...children] 222 | }, []) 223 | .filter(item => { 224 | return !isExternalLink(item.link) 225 | }) 226 | : [] 227 | }, 228 | 229 | sidebar(_, {config}) { 230 | const sidebar = config.sidebar || [] 231 | return typeof sidebar === 'function' ? sidebar(store) : sidebar 232 | }, 233 | 234 | cssVariables(_, {config}) { 235 | return { 236 | ...(config.theme === 'dark' ? darkCssVariables : defaultCssVariables), 237 | ...(typeof config.cssVariables === 'function' 238 | ? config.cssVariables(config.theme) 239 | : config.cssVariables) 240 | } 241 | } 242 | } 243 | }) 244 | 245 | if (process.env.NODE_ENV === 'development' && inBrowser) { 246 | window.store = store 247 | } 248 | 249 | export default store 250 | -------------------------------------------------------------------------------- /website/index.js: -------------------------------------------------------------------------------- 1 | import html from 'html-template-tag' 2 | import googleAnalytics from 'docute-google-analytics' 3 | import Docute from '../src' 4 | import prismLanguages from '../src/utils/prismLanguages' 5 | import ColorBox from './components/ColorBox.vue' 6 | 7 | const SponsorIcon = { 8 | template: html` 9 | <svg style="color:#ea4aaa;" fill="currentColor" viewBox="0 0 12 16" version="1.1" width="12" height="16" aria-hidden="true"><path fill-rule="evenodd" d="M8.727 3C7.091 3 6.001 4.65 6.001 4.65S4.909 3 3.273 3C1.636 3 0 4.1 0 6.3 0 9.6 6 14 6 14s6-4.4 6-7.7C12 4.1 10.364 3 8.727 3z"></path></svg> 10 | ` 11 | } 12 | 13 | new Docute({ 14 | target: 'app', 15 | title: 'Docute', 16 | highlight: ['typescript', 'bash', 'json', 'markdown'], 17 | plugins: [ 18 | process.env.NODE_ENV === 'production' && googleAnalytics('UA-54857209-11') 19 | ].filter(Boolean), 20 | editLinkBase: 'https://github.com/egoist/docute/tree/master/website/docs', 21 | editLinkText: 'Edit this page on GitHub', 22 | router: { 23 | mode: 'history' 24 | }, 25 | detectSystemDarkTheme: true, 26 | darkThemeToggler: true, 27 | sourcePath: '/', 28 | componentMixins: [ 29 | { 30 | data() { 31 | return { 32 | builtinLanguages: prismLanguages.builtin, 33 | deps: __DEPS__ 34 | } 35 | }, 36 | methods: { 37 | insertCustomFontsCSS() { 38 | const ID = 'custom-fonts-css' 39 | const existing = document.getElementById(ID) 40 | if (existing) { 41 | existing.parentNode.removeChild(existing) 42 | } else { 43 | const style = document.createElement('style') 44 | style.id = ID 45 | style.textContent = ` 46 | /* Import desired font from Google fonts */ 47 | @import url('https://fonts.googleapis.com/css?family=Lato'); 48 | 49 | /* Apply the font to body (to override the default one) */ 50 | body { 51 | font-family: Lato, sans-serif; 52 | } 53 | ` 54 | document.head.appendChild(style) 55 | } 56 | } 57 | }, 58 | components: { 59 | ColorBox 60 | } 61 | } 62 | ], 63 | versions: { 64 | 'v4 (Latest)': { 65 | link: '/' 66 | }, 67 | v3: { 68 | link: 'https://docute3.egoist.dev' 69 | } 70 | }, 71 | nav: [ 72 | { 73 | title: 'Home', 74 | link: '/' 75 | }, 76 | { 77 | title: 'GitHub', 78 | link: 'https://github.com/egoist/docute' 79 | } 80 | ], 81 | sidebar: [ 82 | { 83 | title: 'Guide', 84 | children: [ 85 | { 86 | title: 'Introduction', 87 | link: '/' 88 | }, 89 | { 90 | title: 'Customization', 91 | link: '/guide/customization' 92 | }, 93 | { 94 | title: 'Markdown Features', 95 | link: '/guide/markdown-features' 96 | }, 97 | { 98 | title: 'Use Vue in Markdown', 99 | link: '/guide/use-vue-in-markdown' 100 | }, 101 | { 102 | title: 'Internationalization', 103 | link: '/guide/internationalization' 104 | }, 105 | { 106 | title: 'Plugin', 107 | link: '/guide/plugin' 108 | }, 109 | { 110 | title: 'Deployment', 111 | link: '/guide/deployment' 112 | } 113 | ] 114 | }, 115 | { 116 | title: 'Advanced', 117 | children: [ 118 | { 119 | title: 'Use With Bundlers', 120 | link: '/guide/use-with-bundlers' 121 | }, 122 | { 123 | title: 'Offline Support', 124 | link: '/guide/offline-support' 125 | } 126 | ] 127 | }, 128 | { 129 | title: 'References', 130 | children: [ 131 | { 132 | title: 'Options', 133 | link: '/options' 134 | }, 135 | { 136 | title: 'Built-in Components', 137 | link: '/builtin-components' 138 | }, 139 | { 140 | title: 'Plugin API', 141 | link: '/plugin-api' 142 | } 143 | ] 144 | }, 145 | { 146 | title: 'Credits', 147 | link: '/credits' 148 | } 149 | ], 150 | overrides: { 151 | '/': { 152 | language: 'English' 153 | }, 154 | '/zh/': { 155 | language: 'Chinese', 156 | editLinkText: '在 GitHub 上编辑此页', 157 | nav: [ 158 | { 159 | title: '首页', 160 | link: '/zh/' 161 | }, 162 | { 163 | title: 'GitHub', 164 | link: 'https://github.com/egoist/docute' 165 | } 166 | ], 167 | sidebar: [ 168 | { 169 | title: '指南', 170 | children: [ 171 | { 172 | title: '介绍', 173 | link: '/zh' 174 | }, 175 | { 176 | title: '自定义', 177 | link: '/zh/guide/customization' 178 | }, 179 | { 180 | title: 'Markdown 功能', 181 | link: '/zh/guide/markdown-features' 182 | }, 183 | { 184 | title: '在 Markdown 中使用 Vue', 185 | link: '/zh/guide/use-vue-in-markdown' 186 | }, 187 | { 188 | title: '国际化', 189 | link: '/zh/guide/internationalization' 190 | }, 191 | { 192 | title: '插件', 193 | link: '/zh/guide/plugin' 194 | }, 195 | { 196 | title: '部署', 197 | link: '/zh/guide/deployment' 198 | } 199 | ] 200 | }, 201 | { 202 | title: '进阶', 203 | children: [ 204 | { 205 | title: '使用打包工具', 206 | link: '/zh/guide/use-with-bundlers' 207 | }, 208 | { 209 | title: '离线支持', 210 | link: '/zh/guide/offline-support' 211 | } 212 | ] 213 | }, 214 | { 215 | title: '参考', 216 | children: [ 217 | { 218 | title: '配置项', 219 | link: '/zh/options' 220 | }, 221 | { 222 | title: '内置组件', 223 | link: '/zh/builtin-components' 224 | }, 225 | { 226 | title: '插件 API', 227 | link: '/zh/plugin-api' 228 | } 229 | ] 230 | }, 231 | { 232 | title: '致谢', 233 | link: '/zh/credits' 234 | } 235 | ] 236 | } 237 | }, 238 | footer: ` 239 | <div style="border-top:1px solid var(--border-color);padding-top:30px;margin: 40px 0;color:#999999;font-size: .9rem;"> 240 | © ${new Date().getFullYear()} Developed by <a href="https://egoist.dev" target="_blank">EGOIST</a>. Released under MIT license. 241 | </div> 242 | `, 243 | banner: { 244 | template: html` 245 | <div class="docute-banner"> 246 | <note :label="false" 247 | ><SponsorIcon width="16" height="16" style="position:relative;top:2px;margin-right:8px;" />Sponsor the author on 248 | <a href="https://github.com/sponsors/egoist" target="_blank" 249 | >GitHub<ExternalLinkIcon /></a 250 | > to support Docute.</note 251 | > 252 | </div> 253 | `, 254 | components: { 255 | SponsorIcon 256 | } 257 | } 258 | }) 259 | 260 | Vue.component('ReverseText', { 261 | props: { 262 | text: { 263 | type: String, 264 | required: true 265 | } 266 | }, 267 | template: html` 268 | <div class="reverse-text"> 269 | {{ reversedText }} 270 | <v-style> 271 | .reverse-text { 272 | border: 1px solid var(--border-color); 273 | padding: 20px; 274 | font-weight: bold; 275 | border-radius: 4px; 276 | } 277 | </v-style> 278 | </div> 279 | `, 280 | computed: { 281 | reversedText() { 282 | return this.text 283 | .split('') 284 | .reverse() 285 | .join('') 286 | } 287 | } 288 | }) 289 | 290 | if (process.env.NODE_ENV === 'production' && 'serviceWorker' in navigator) { 291 | navigator.serviceWorker.register('/sw.js') 292 | } 293 | -------------------------------------------------------------------------------- /src/plugins/dark-theme-toggler/DarkThemeToggler.vue: -------------------------------------------------------------------------------- 1 | <template> 2 | <div class="dark-theme-toggler"> 3 | <div class="toggle" :class="{checked: dark}" @click="handleChange"> 4 | <div class="toggle-track"> 5 | <div class="toggle-track-check"> 6 | <img 7 | src="data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAACAAAAAgCAYAAABzenr0AAAAAXNSR0IArs4c6QAAAAlwSFlzAAALEwAACxMBAJqcGAAAAVlpVFh0WE1MOmNvbS5hZG9iZS54bXAAAAAAADx4OnhtcG1ldGEgeG1sbnM6eD0iYWRvYmU6bnM6bWV0YS8iIHg6eG1wdGs9IlhNUCBDb3JlIDUuNC4wIj4KICAgPHJkZjpSREYgeG1sbnM6cmRmPSJodHRwOi8vd3d3LnczLm9yZy8xOTk5LzAyLzIyLXJkZi1zeW50YXgtbnMjIj4KICAgICAgPHJkZjpEZXNjcmlwdGlvbiByZGY6YWJvdXQ9IiIKICAgICAgICAgICAgeG1sbnM6dGlmZj0iaHR0cDovL25zLmFkb2JlLmNvbS90aWZmLzEuMC8iPgogICAgICAgICA8dGlmZjpPcmllbnRhdGlvbj4xPC90aWZmOk9yaWVudGF0aW9uPgogICAgICA8L3JkZjpEZXNjcmlwdGlvbj4KICAgPC9yZGY6UkRGPgo8L3g6eG1wbWV0YT4KTMInWQAABlJJREFUWAm1V3tsFEUcntnXvXu0tBWo1ZZHihBjCEWqkHiNaMLDRKOtQSKaiCFKQtS/SbxiFCHGCIkmkBSMwZhQNTFoQZD0DFiwtCDFAkdDqBBBKFj63rvdnfH7zfVo5aFBj0l2Z/dm5vd98/0es8dYjlpr62azufnDQNZcU1PciMfjWvb9rvZSMk4Ayfb36pLH13189GC8LAtIRLLPt+pzwrCuLq4ISEv/gHmitrAwfPbEkXc/ad4dL6iujrvyX0jcitgd/yZlZqftP6995Mr5TVLa22Tn8XVX2g/XLSRjUu7Q79jonS7I7hS7/0oOb5VyqF52n98oj7esXX07EjlxwXWisRmSnm3b29TTM8iYrjmFBWExubxwY/uhNas4r/WySl1fc5cetDMd7ydl+lMJJRw5WC8ud62Xx5rfepzwxgZmbhUYNS5Stvsj4yo2GXJEFBVHWDBkfdbR9HpYBaaUajDnBLKKpl1xRKYcgGtMCqEzTaSnThk/SQT0uJqTqFNBmXMCsZE48DzRZRMBRjv1GHNdk3HBImF9ZUvTyxM40pMKVc4JZBXQOLOFoDeKSxdp6HIQcO4rjYT9fn0pjbz9GLt7BAAODmjSVReXUMFzNW5x5vfxp2mIxZjIuQKJxAmFa+is2DQJJQ0JyBVExNOYcJnPxx/6/utnijmP555ALEagKAGGnGn64QORBjARcIA/yJk7JMJBLRrNtybTvH88KGjCf2jK86bhzmMcwDKFZEQvbIhxFYhChoMWMzU2iWznlIBEVJOsP+1bdX/ALx9l7jApADeDAEcMkE90JnUmmGl4USKQ0xhoW3JB5XY0YrxYWhLwMZZypUyjDGH35AbNwgUGiFBPpuGbHCpAOV1ZGXf2f/taftAv31DyeymN2d1IhAFAwTOmnzF/kKcdh3me7CYCOVNgycju84u8DeVlwfFq9/ZlTfldYrMUjOlrkjkD+rU+WzCROkcEchIDHR011syZW9JHD7y07N6JvhWMpz3pugaTkB6lWFVCKkhck0zzeMp2utq+uHrmfxOgoCO/Z8CXPlEQ1bdH8wgvhSIkEG0ICcQeExIFGdimjvKka7btJFZuaXOammIGKUCFQ53j9EN1dYKWqHf0t2w407W2tgs6h89ZnImjB55flh81tt9XirjjDuSl+oIPRQ0iWPgNZ5GqTqbBe3vSzEl5n5PhWKwocyR2HlqYN61qV18WjYjE8JLARZPQsUSim8foIRYTlGr02Ly7piASFRtKJ4VfieYhxdS2JcDVMN6xVOKZyrCGm8b108lrLRVzvptLH7IoEFLFANes6KnDi+uxfmvFnF17oALq5u1agu3/YfHkcSFzeSggV5eXRfIB7CHNcO5SUI+Ih5Ir7f4MAV9IqdFzdZgNpZw1Gcs1mNvgGbTbqQ9/cz7ZuuhgyYRQ49ljTyWHhr2DwpNHHFf+5gnWZ3Bharo+0TD5dNMw5vv9RlVpSRDHK4TlnoukhtYApuOHejSZQuo5g/A9BysdKRCyLl6062fN37OXMDlvUJtUrtmxo0avrW3wTrYs3jJ9RvRVChrmSmanPMpX2OXMsmDGh6AiEIwBAlvkOqIdBy+8JyAz8pz7QxiDth4KDy5uAlwzrWTnwC8Vc4KVAMZ3YUZ+IqoIjP3h5KFFX1ZMy3uW+7RhEDHgTi0zC9rS7uhPCDiNrGFyqBeERtKN/B0YlyFCkw0NJ5C0Ojv7zvT1a1WV1TuvZDdL4NTgB7CASYpsen6gqvG5jmTf5qHedADgkBl3D0nkSgNhZACDyi0FUKZRr3IdRjgN4WPPoFMIIegIK3mqd38fS80mcJKelM4szNyzZtQbkchGePuBRS8Eg9pHU8ojRQpSqs+ajAIwTjjUMQ/nvTNM0kicwYxZIYMh/891DYi+fvedB+c1xsm4lDU6ya+Axtz+RiAzEVYbajQOpq17F0R9QevNcEhfcU+xvyQQUalGJBSesqOkgPQ4YNyUZL9fSvUPDjoNAwN8/dwFjaczNkc3ptaMud1EIDtGcmXTcefO2cGSvKIFfp/2JIJxlq7xEl3nVPM4fDeIbPkD16/ptNc0bDu7qxbsu0R2JGywWMIjF2ft3tjfloAyQAGXiOn8hrqwbVvMXzaO+QeHXP6nF0wvX74Hf4NGG5GPjSlYoyM3P/0FbCT6zvM/yYoAAAAASUVORK5CYII=" 8 | width="16" 9 | height="16" 10 | role="presentation" 11 | style="pointer-events: none;" 12 | /> 13 | </div> 14 | <div class="toggle-track-x"> 15 | <img 16 | src="data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAACAAAAAgCAYAAABzenr0AAAAAXNSR0IArs4c6QAAAAlwSFlzAAALEwAACxMBAJqcGAAAAVlpVFh0WE1MOmNvbS5hZG9iZS54bXAAAAAAADx4OnhtcG1ldGEgeG1sbnM6eD0iYWRvYmU6bnM6bWV0YS8iIHg6eG1wdGs9IlhNUCBDb3JlIDUuNC4wIj4KICAgPHJkZjpSREYgeG1sbnM6cmRmPSJodHRwOi8vd3d3LnczLm9yZy8xOTk5LzAyLzIyLXJkZi1zeW50YXgtbnMjIj4KICAgICAgPHJkZjpEZXNjcmlwdGlvbiByZGY6YWJvdXQ9IiIKICAgICAgICAgICAgeG1sbnM6dGlmZj0iaHR0cDovL25zLmFkb2JlLmNvbS90aWZmLzEuMC8iPgogICAgICAgICA8dGlmZjpPcmllbnRhdGlvbj4xPC90aWZmOk9yaWVudGF0aW9uPgogICAgICA8L3JkZjpEZXNjcmlwdGlvbj4KICAgPC9yZGY6UkRGPgo8L3g6eG1wbWV0YT4KTMInWQAABwNJREFUWAmtV1tsFFUY/s6Z2d22zLYlZakUCRVaQcqlWIiCiS1gTEB9UAO+GR9En3iQGI0xJiSiRB98MjEq8cEQTSBeHhQM0V7whtEGDWC90BYitxahtNtu25058/v/ZzvLbilawJNM5+yZ89+//1LgJhYRNLW1uDfBAvpGiIk2O5auvfFxqIH3ZJ8/u06GN6Z9+wVl5SjcD1IbZa/UPkPyYl2uR4dreoD2bnbYxTlBBRytkHXtAREphP5KuH4lddx9h70yxX05t7yYXwGb6W8nx1jibpl2rFlGBxcG9M18okOrn7Bnk/BAO/4bI0UeEE1zjBp3UmvjOxJXJdaKN/ZiIu4tOZrAb4aTdZAZArKmWeiiJZ6jt5tiagdCS9+6cgO1Ne6Mvhe+ixTIfyDVhipnK9p+P0Edqx9RW/YZtQVGmOLChRxNNlyPsTEgPQKMB3dbEHa0h1awYmQ83enTd2vmUtvKd1Glv2RkzBb+kZGRrKtjzG60Wguhd/lJZBingbcfWWe72vjT75bJDrhYtvA0hrurETDr5HyF2Knb1MM4ab//xIoOqueA0edRnkkinTyJdYvqLFDZO4zUPFCvVoDjJq4T7TE61IWh4x5KqxX5KVKkX8WZ/t2ov2cb3MHt4dhIyOxIJxJOOF6xRx/99BksXLoecWcXytILMNBDqKpnGZWPquYfPxY8iXGR9fK+SgFrgcRPXPjVqhehL+3EmZ5RGJQi1QBU8TPThQnOQzm+5UXGIcetUeEAfP13VwzpI+w1jGJWdSliNfvVhiMPiOsllJag4M/UGHiqM6dlBb2OTLKHHV6KkvogrJ4XhBWniWK/Gp1MQyf93FOeUXKmKk/FzJxbQtKLjFXYT4USupy8fQVir2ynVEBiZMG0qtOHMS/AW4Gwrk7BG3C1F0B5nqNKE0CME4MfVRLPnXkBKe+ipvoFhNQywOhdghvLi0F8ReyVXV4BKTBRbbe5f64zR/DHsdZw1hJfeWlHl/GNRJzDxrd5m192z78TMaVnKELZoINZS4BzQ7vtnZljSnha/pPCbkuxzXcupYwI5tIeCpGc0Yp9tWHZQy/rmYhRfNgg4bHJBYLzGkxsRJF4XKlE2jBOHNSv3kY7Tj6vthzPFl61BrYwqFlmEQhtSVXmLiksxLmtRgYXI1ULU61JJ4eVKmG3/5sCVgpbMT6OMJ2E08/29Xf3w6v4FnHdCjfWgXu/O8Z5mLdCkeRs2khHe1DqOtQwbHWTAnM5S2HNmhALYo5KjkPFrMMKjZl6HxhWIAb0BqE+/73GrBRQUsKYiBu4JX8ycI6wtw+i5ef3NZpsrKVSHYCP37jwGDgeE1SA0S/xtl5SU2fs1ApEp0qTLVRjgyycDSsLHMSwmFltZMStR3uLLg6BdLhDa5dC6ryU2pHBe1BVO9tUcwfitJt2CLJZUHoG6T7Op75u0IyK31TCPcwFqgPk/KCaD3dFOuZBCO7xvCT/j048b3I3c7F2+WuOW7qdgkucFYlcQ4qop3yzTX7WaKfOCccye3Ts1Etq0+a/BHCF1yPgF3tAUkR6OrtGmo6gl94qqcXKh3rDyrOkPa58URoWcov2Mo6M+0QjrqKB+b7++oMa9Sz+ZkM0mie6aAtnGUvhmxaI+TogPOSQedgWioGSHFLn3v4kLh4HRspNmOGv41k+55siLFp2z6xYeJjhljFcbmxJlr4ga06TbevSByz/glQq4BJx46/c+237PbBqEYKxX3HpmKZEnQnr65X20hqJYaNcLoFOLiJk2LuBbyg7Q0OEn+hm0P3honxFD6rdxYorKpeIoi4YSSvyQHQIbM5t4+YNxLj/OxhVOOE4585qGpjnq+wSx6Q9CtNxTjd5klB+g6Mv36r0+b9cZFi44WYkHdG2ZWb3TtOUOXyVAlKlpGvJIAJ3eBMyfYS5C0qRZGtC85j+4sOasDe9xznPYezhhO/2Q6eP2fSOvYHOjtuQ1a9Q1VKynVDaMc8E0tptdxUsTFpFIYjcZKcbnoaQTNdiqCwNlL4G7oziSqGnT1ALf34vhk4R5zU3qYV9ONp9K88RtouShE68JwaU8dFw5W617shWa9ykeaBIn2hcsvPgL00k45QdTCZuSVcTRNs+8fnyLvooQfR5iujAnR9bxfY2xOVOxFS8SK3Le0l48VyYu1M8HRe5JD8wKPTjYnifaK3Wfn/GChYQ8ZAi6WRzWgqLV5YrsVLnZaVSoXU1g9gOIDwFySiGi+Zdrnzr7J3r+SMuszlcQCRn8lNGcTuSy2jOI7o9mxjZo+vR3ej3tN+ifRSOyUTS0+VMOid93cCubeiy/6TImS0QxRSCq2vxKr45zV+FQnjWH6D2xg+E9EatLcLAdHTgtGGD80D6jM0+aOl4wJgO/f96R2aJKCQ3yvgftRhdFMOpd6oAAAAASUVORK5CYII=" 17 | width="16" 18 | height="16" 19 | role="presentation" 20 | style="pointer-events: none;" 21 | /> 22 | </div> 23 | </div> 24 | <div class="toggle-thumb"></div> 25 | </div> 26 | <input 27 | type="checkbox" 28 | class="toggler-screen-reader-only" 29 | :checked="dark" 30 | aria-label="Switch between Dark and Default theme" 31 | /> 32 | </div> 33 | </template> 34 | 35 | <script> 36 | export default { 37 | data() { 38 | const themeStore = localStorage.getItem('docute:theme') 39 | const dark = 40 | 'dark' in this.$route.query 41 | ? true 42 | : themeStore === 'dark' 43 | ? true 44 | : themeStore === 'default' 45 | ? false 46 | : this.$store.getters.config.theme === 'dark' 47 | return { 48 | dark 49 | } 50 | }, 51 | 52 | created() { 53 | this.$store.commit('SET_THEME', this.dark ? 'dark' : 'default') 54 | }, 55 | 56 | methods: { 57 | handleChange() { 58 | const prevTheme = this.$store.getters.config.theme 59 | this.dark = !this.dark 60 | this.$store.commit( 61 | 'SET_THEME', 62 | this.dark ? 'dark' : prevTheme === 'dark' ? 'default' : prevTheme 63 | ) 64 | localStorage.setItem('docute:theme', this.dark ? 'dark' : 'default') 65 | } 66 | } 67 | } 68 | </script> 69 | 70 | <style scoped> 71 | .dark-theme-toggler { 72 | display: flex; 73 | align-items: center; 74 | height: 100%; 75 | 76 | @media screen and (max-width: 768px) { 77 | padding: 0 20px; 78 | } 79 | 80 | @nest [data-position='sidebar:post-end'] & { 81 | padding: 0 20px; 82 | } 83 | } 84 | 85 | .toggle { 86 | touch-action: pan-x; 87 | display: inline-block; 88 | position: relative; 89 | cursor: pointer; 90 | background-color: transparent; 91 | border: 0; 92 | padding: 0; 93 | user-select: none; 94 | -webkit-tap-highlight-color: transparent; 95 | } 96 | 97 | .toggle-track { 98 | width: 50px; 99 | height: 22px; 100 | padding: 0; 101 | border-radius: 30px; 102 | background-color: #0f1114; 103 | transition: all 0.2s ease; 104 | } 105 | 106 | .toggle-track-check { 107 | position: absolute; 108 | width: 17px; 109 | height: 17px; 110 | left: 5px; 111 | top: 0; 112 | bottom: 0; 113 | margin-top: auto; 114 | margin-bottom: auto; 115 | line-height: 0; 116 | opacity: 0; 117 | transition: opacity 0.25s ease; 118 | } 119 | 120 | .toggle-track-x { 121 | position: absolute; 122 | width: 17px; 123 | height: 17px; 124 | right: 5px; 125 | top: 0; 126 | bottom: 0; 127 | margin-top: auto; 128 | margin-bottom: auto; 129 | line-height: 0; 130 | } 131 | 132 | .toggle-thumb { 133 | position: absolute; 134 | top: 2px; 135 | left: 2px; 136 | width: 18px; 137 | height: 18px; 138 | border-radius: 50%; 139 | background-color: white; 140 | box-sizing: border-box; 141 | transition: all 0.5s cubic-bezier(0.23, 1, 0.32, 1) 0ms; 142 | transform: translateX(0); 143 | } 144 | 145 | .checked .toggle-track-check, 146 | .toggle-track-x { 147 | opacity: 1; 148 | transition: opacity 0.25s ease; 149 | } 150 | 151 | .checked .toggle-track-x { 152 | opacity: 0; 153 | } 154 | 155 | .checked .toggle-thumb { 156 | transform: translateX(26px); 157 | border-color: #19ab27; 158 | } 159 | 160 | .toggler-screen-reader-only { 161 | width: 1px; 162 | height: 1px; 163 | clip: rect(0 0 0 0); 164 | position: absolute; 165 | overflow: hidden; 166 | border: none; 167 | margin: -1px; 168 | } 169 | </style> 170 | --------------------------------------------------------------------------------