├── .babelrc.js
├── .editorconfig
├── .gitattributes
├── .github
└── FUNDING.yml
├── .gitignore
├── .prettierrc
├── LICENSE
├── README.md
├── build
├── generate-required-languages.js
├── poi.lib.config.js
├── poi.website.config.js
└── vue-compile.config.js
├── circle.yml
├── now.json
├── package-lock.json
├── package.json
├── postcss.config.js
├── release.config.js
├── src
├── PluginAPI.js
├── components
│ ├── Badge.vue
│ ├── DocuteSelect.vue
│ ├── EditLink.vue
│ ├── Gist.vue
│ ├── Header.vue
│ ├── HeaderNav.vue
│ ├── ImageZoom.vue
│ ├── InjectedComponents.js
│ ├── Loading.vue
│ ├── Note.vue
│ ├── PageToc.vue
│ ├── PrevNextLinks.vue
│ ├── Root.vue
│ ├── Sidebar.vue
│ ├── SidebarItem.vue
│ ├── SidebarMask.vue
│ ├── SidebarToggle.vue
│ ├── UniLink.vue
│ └── icons
│ │ └── ExternalLinkIcon.vue
├── css
│ ├── global.css
│ ├── injected-components.css
│ ├── main.css
│ ├── page-content.css
│ └── prism.css
├── event.js
├── hooks.js
├── index.js
├── plugins
│ ├── banner-footer
│ │ └── index.js
│ ├── dark-theme-toggler
│ │ ├── DarkThemeToggler.vue
│ │ └── index.js
│ ├── evaluateContent
│ │ └── index.js
│ ├── i18n
│ │ ├── LanguageSelector.vue
│ │ └── index.js
│ ├── search
│ │ ├── SearchBar.vue
│ │ └── index.js
│ └── versions
│ │ ├── VersionsSelector.vue
│ │ └── index.js
├── router.js
├── store.js
├── utils
│ ├── __test__
│ │ └── index.test.js
│ ├── alternativeComponents.js
│ ├── constants.js
│ ├── cssVariables.js
│ ├── highlight.js
│ ├── index.js
│ ├── load.js
│ ├── marked.js
│ ├── markedRenderer.js
│ ├── parseCodeOptions.js
│ ├── prismLanguages.json
│ └── removeMarkdownExtension.js
└── views
│ └── Home.vue
└── website
├── components
└── ColorBox.vue
├── docs
├── README.md
├── builtin-components.md
├── credits.md
├── guide
│ ├── customization.md
│ ├── deployment.md
│ ├── internationalization.md
│ ├── markdown-features.md
│ ├── offline-support.md
│ ├── plugin.md
│ ├── use-vue-in-markdown.md
│ └── use-with-bundlers.md
├── options.md
├── plugin-api.md
├── sw.js
└── zh
│ ├── README.md
│ ├── builtin-components.md
│ ├── credits.md
│ ├── guide
│ ├── customization.md
│ ├── deployment.md
│ ├── internationalization.md
│ ├── markdown-features.md
│ ├── offline-support.md
│ ├── plugin.md
│ ├── use-vue-in-markdown.md
│ └── use-with-bundlers.md
│ ├── options.md
│ └── plugin-api.md
└── index.js
/.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
--------------------------------------------------------------------------------
/.gitattributes:
--------------------------------------------------------------------------------
1 | * text=auto
2 |
--------------------------------------------------------------------------------
/.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 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | node_modules
2 | dist
3 | /lib
4 |
--------------------------------------------------------------------------------
/.prettierrc:
--------------------------------------------------------------------------------
1 | {
2 | "semi": false,
3 | "singleQuote": true,
4 | "useTabs": false
5 | }
6 |
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------
/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 | [](https://npm.im/docute) [](https://www.jsdelivr.com/package/npm/docute) [](https://circleci.com/gh/egoist/docute/tree/master) [](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 |
--------------------------------------------------------------------------------
/build/generate-required-languages.js:
--------------------------------------------------------------------------------
1 | const fs = require('fs')
2 |
3 | let {languages} = require('prismjs/components')
4 |
5 | languages = Object.keys(languages).reduce((res, name) => {
6 | if (languages[name].require) {
7 | res[name] = languages[name].require
8 | }
9 | return res
10 | }, {})
11 | languages.builtin = Object.keys(require('prismjs').languages).filter(lang => {
12 | return !['extend', 'insertBefore', 'DFS'].includes(lang)
13 | })
14 |
15 | fs.writeFileSync(
16 | './src/utils/prismLanguages.json',
17 | JSON.stringify(languages, null, 2),
18 | 'utf8'
19 | )
20 |
--------------------------------------------------------------------------------
/build/poi.lib.config.js:
--------------------------------------------------------------------------------
1 | const pkg = require('../package')
2 |
3 | module.exports = {
4 | entry: 'src/index.js',
5 | output: {
6 | format: 'umd',
7 | moduleName: 'Docute',
8 | fileNames: {
9 | js: 'docute.js',
10 | css: 'docute.css'
11 | },
12 | clean: false
13 | },
14 | constants: {
15 | __DOCUTE_VERSION__: JSON.stringify(pkg.version),
16 | __PRISM_VERSION__: JSON.stringify(require('prismjs/package').version)
17 | },
18 | chainWebpack(config) {
19 | config.resolve.alias.set('vue$', 'vue/dist/vue.esm.js')
20 | config.output.libraryExport('default')
21 | config.output.globalObject('this')
22 | }
23 | }
24 |
--------------------------------------------------------------------------------
/build/poi.website.config.js:
--------------------------------------------------------------------------------
1 | const path = require('path')
2 | const pkg = require('../package')
3 |
4 | module.exports = {
5 | entry: 'website/index.js',
6 | output: {
7 | dir: 'website/dist',
8 | html: {
9 | title: 'Docute'
10 | }
11 | },
12 | publicFolder: path.join(__dirname, '../website/docs'),
13 | chainWebpack(config) {
14 | config.resolve.alias.set('vue$', 'vue/dist/vue.esm.js')
15 | },
16 | constants: {
17 | __DOCUTE_VERSION__: JSON.stringify(pkg.version),
18 | __PRISM_VERSION__: JSON.stringify(require('prismjs/package').version),
19 | __DEPS__: JSON.stringify(Object.keys(pkg.dependencies))
20 | }
21 | }
22 |
--------------------------------------------------------------------------------
/build/vue-compile.config.js:
--------------------------------------------------------------------------------
1 | module.exports = {
2 | outDir: 'lib',
3 | include: '**',
4 | constants: {
5 | __DOCUTE_VERSION__: JSON.stringify(require('../package').version),
6 | __PRISM_VERSION__: JSON.stringify(require('prismjs/package').version)
7 | }
8 | }
9 |
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------
/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/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/components/Badge.vue:
--------------------------------------------------------------------------------
1 |
2 |
6 |
7 |
8 |
9 |
10 |
23 |
24 |
54 |
--------------------------------------------------------------------------------
/src/components/DocuteSelect.vue:
--------------------------------------------------------------------------------
1 |
2 |
20 |
21 |
22 |
36 |
37 |
97 |
--------------------------------------------------------------------------------
/src/components/EditLink.vue:
--------------------------------------------------------------------------------
1 |
2 |
23 |
24 |
25 |
38 |
39 |
55 |
--------------------------------------------------------------------------------
/src/components/Gist.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
37 |
38 |
43 |
--------------------------------------------------------------------------------
/src/components/Header.vue:
--------------------------------------------------------------------------------
1 |
2 |
20 |
21 |
22 |
73 |
74 |
144 |
--------------------------------------------------------------------------------
/src/components/HeaderNav.vue:
--------------------------------------------------------------------------------
1 |
2 |
34 |
35 |
36 |
57 |
58 |
197 |
--------------------------------------------------------------------------------
/src/components/ImageZoom.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 |
![]()
4 |
5 |
6 |
7 |
47 |
48 |
60 |
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------
/src/components/Loading.vue:
--------------------------------------------------------------------------------
1 |
2 |
6 |
7 |
8 |
13 |
14 |
65 |
--------------------------------------------------------------------------------
/src/components/Note.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 | {{ props.label === true ? props.type : props.label }}:
6 |
7 |
8 |
9 |
10 |
25 |
26 |
81 |
--------------------------------------------------------------------------------
/src/components/PageToc.vue:
--------------------------------------------------------------------------------
1 |
2 |
12 |
20 |
21 |
22 |
23 |
33 |
34 |
60 |
--------------------------------------------------------------------------------
/src/components/PrevNextLinks.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | ← {{ prevLinkItem.title }}
6 |
7 |
8 |
9 |
10 |
11 | {{ nextLinkItem.title }} →
12 |
13 |
14 |
15 |
16 |
17 |
56 |
57 |
77 |
--------------------------------------------------------------------------------
/src/components/Root.vue:
--------------------------------------------------------------------------------
1 |
58 |
59 |
60 |
--------------------------------------------------------------------------------
/src/components/Sidebar.vue:
--------------------------------------------------------------------------------
1 |
2 |
25 |
26 |
27 |
84 |
85 |
120 |
--------------------------------------------------------------------------------
/src/components/SidebarItem.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 |
9 |
10 |
23 |
24 |
{{ item.title }}
25 |
26 |
{{ item.title }}
33 |
36 |
37 |
41 |
42 |
{{ link.title }}
50 |
51 |
52 |
53 |
54 |
55 |
56 |
110 |
111 |
214 |
--------------------------------------------------------------------------------
/src/components/SidebarMask.vue:
--------------------------------------------------------------------------------
1 |
2 |
7 |
8 |
9 |
19 |
--------------------------------------------------------------------------------
/src/components/SidebarToggle.vue:
--------------------------------------------------------------------------------
1 |
2 |
10 |
11 |
12 |
31 |
32 |
53 |
--------------------------------------------------------------------------------
/src/components/UniLink.vue:
--------------------------------------------------------------------------------
1 |
46 |
47 |
52 |
--------------------------------------------------------------------------------
/src/components/icons/ExternalLinkIcon.vue:
--------------------------------------------------------------------------------
1 |
2 |
21 |
22 |
23 |
28 |
--------------------------------------------------------------------------------
/src/css/global.css:
--------------------------------------------------------------------------------
1 | @import "main.css";
2 | @import "injected-components.css";
3 |
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------
/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/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 |
--------------------------------------------------------------------------------
/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/event.js:
--------------------------------------------------------------------------------
1 | import Vue from 'vue'
2 |
3 | export default new Vue()
4 |
--------------------------------------------------------------------------------
/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/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 |
--------------------------------------------------------------------------------
/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/plugins/dark-theme-toggler/DarkThemeToggler.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |

13 |
14 |
15 |

22 |
23 |
24 |
25 |
26 |
32 |
33 |
34 |
35 |
69 |
70 |
170 |
--------------------------------------------------------------------------------
/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/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/i18n/LanguageSelector.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 |
8 |
9 |
16 |
17 |
18 |
19 |
20 |
43 |
44 |
50 |
--------------------------------------------------------------------------------
/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/search/SearchBar.vue:
--------------------------------------------------------------------------------
1 |
2 |
39 |
40 |
41 |
93 |
94 |
208 |
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------
/src/plugins/versions/VersionsSelector.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 |
8 |
9 |
17 |
18 |
19 |
20 |
21 |
49 |
50 |
56 |
--------------------------------------------------------------------------------
/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/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/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 |
--------------------------------------------------------------------------------
/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/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 |
--------------------------------------------------------------------------------
/src/utils/constants.js:
--------------------------------------------------------------------------------
1 | export const INITIAL_STATE_NAME = '__DOCUTE_INITIAL_STATE__'
2 |
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------
/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/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/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 |
39 | ${text}${tag}>`
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/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/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 | }
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------
/src/views/Home.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
16 |
17 |
18 |
19 |
20 |
21 |
22 |
23 |
24 |
25 |
26 |
27 |
28 |
29 |
30 |
31 |
32 |
33 |
34 |
35 |
36 |
180 |
181 |
182 |
183 |
184 |
232 |
--------------------------------------------------------------------------------
/website/components/ColorBox.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
10 |
11 |
12 |
20 |
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------
/website/docs/builtin-components.md:
--------------------------------------------------------------------------------
1 | # Built-in Components
2 |
3 | Docute comes with a set of built-in Vue components.
4 |
5 | ## ``
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 |
18 |
19 | Example:
20 |
21 | ```markdown
22 |
27 | ```
28 |
29 |
30 |
31 | ## ``
32 |
33 | A small count and labeling component.
34 |
35 | | Prop | Type | Default | Description |
36 | | -------- | -------------------------------------------------------------------- | ------- | ----------------------- |
37 | | type | 'tip' | 'success' | 'warning' | 'danger'
| N/A | Badge type |
38 | | color | `string` | N/A | Custom background color |
39 | | children | `string` | N/A | Badge text |
40 |
41 |
42 |
43 | Example:
44 |
45 | ```markdown
46 | - Feature 1 Badge
47 | - Feature 2 Tip
48 | - Feature 3 Success
49 | - Feature 4 Warning
50 | - Feature 5 Danger
51 | - Feature 6 Custom Color
52 | ```
53 |
54 | - Feature 1 Badge
55 | - Feature 2 Tip
56 | - Feature 3 Success
57 | - Feature 4 Warning
58 | - Feature 5 Danger
59 | - Feature 6 Custom Color
60 |
61 | ## ``
62 |
63 | Colored note blocks, to emphasize part of your page.
64 |
65 | | Prop | Type | Default | Description |
66 | | -------- | -------------------------------------------------------------------- | ------------------- | ------------------------------------------------- |
67 | | type | 'tip' | 'warning' | 'danger' | 'success'
| 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 |
72 |
73 | Examples:
74 |
75 | ```markdown
76 |
77 |
78 | This is a note that details something important.
79 | [A link to helpful information.](https://docute.org)
80 |
81 |
82 |
83 |
84 |
85 |
86 | This is a tip for something that is possible.
87 |
88 |
89 |
90 |
91 |
92 |
93 | This is a warning for something very important.
94 |
95 |
96 |
97 |
98 |
99 |
100 | This is a danger for something to take action for.
101 |
102 |
103 | ```
104 |
105 |
106 |
107 | This is a note that details something important.
108 | [A link to helpful information.](https://docute.org)
109 |
110 |
111 |
112 |
113 |
114 |
115 | This is a tip for something that is possible.
116 |
117 |
118 |
119 |
120 |
121 |
122 | This is a warning for something very important.
123 |
124 |
125 |
126 |
127 |
128 |
129 | This is a danger for something to take action for.
130 |
131 |
132 |
133 | ## ``
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 |
145 | ```
146 |
147 |
148 |
149 | ## ``
150 |
151 | A customized `