├── .eslintrc.js ├── .gitignore ├── LICENSE ├── README.md ├── package.json ├── snapshot.png ├── src ├── assets │ └── light.css ├── index.js └── toc.js └── webpack.config.js /.eslintrc.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | root: true, 3 | env: { 4 | browser: true, 5 | es6: true 6 | }, 7 | extends: [ 8 | 'standard' 9 | ], 10 | globals: { 11 | Atomics: 'readonly', 12 | SharedArrayBuffer: 'readonly' 13 | }, 14 | parserOptions: { 15 | ecmaVersion: 2021, 16 | sourceType: 'module' 17 | }, 18 | rules: { 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | package-lock.json 3 | /dist -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2019 Justin Tien (Yi-Ming Tien) 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # docsify-plugin-toc 2 | 3 | > page's ToC for docsify. 4 | 5 | [![npm](https://img.shields.io/npm/v/docsify-plugin-toc.svg?style=flat-square)](https://www.npmjs.com/package/docsify-plugin-toc) 6 | 7 | [![homepage](./snapshot.png)](https://blog.jiapan.tw "Justin (Jiapan 賈胖) 的 Blog") 8 | 9 | ## Usage 10 | 11 | 1. Configure docsify-plugin-toc: 12 | 13 | ```html 14 | 23 | ``` 24 | 25 | 2. Insert style/script into docsify document: 26 | 27 | ```html 28 | 29 | 30 | 31 | 32 | 33 | 34 | ``` 35 | 36 | ## Options 37 | 38 | | Argument | Type | Description | 39 | | --- | --- | --- | 40 | | `tocMaxLevel` | `number` | The maximum depth of the headings printed on the ToC. If you set `tocMaxLevel` to 3, I recommend you to set `subMaxLevel` to 2 avoid ToC duplication. | 41 | | `target` | `string` | The target heading printed on the ToC. It's used as an argument to query DOM with `querySelectorAll()` | 42 | | `ignoreHeaders` | `string[]` | ignore header name keywrod list, (e.g. ['develop', /develop/i], support Regular expressions) | 43 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "docsify-plugin-toc", 3 | "version": "1.3.2", 4 | "description": "A docsify plugin to display ToC for each page", 5 | "main": "src", 6 | "unpkg": "dist/docsify-plugin-toc.min.js", 7 | "homepage": "https://github.com/justintien/docsify-plugin-toc", 8 | "repository": "git@github.com:justintien/docsify-plugin-toc.git", 9 | "files": [ 10 | "dist/", 11 | "snapshot.png" 12 | ], 13 | "scripts": { 14 | "test": "echo \"Error: no test specified\" && exit 1", 15 | "lint": "eslint --ext .js src", 16 | "build": "webpack" 17 | }, 18 | "keywords": [ 19 | "docsify" 20 | ], 21 | "author": "Justin Tien", 22 | "license": "MIT", 23 | "devDependencies": { 24 | "babel-eslint": "^10.1.0", 25 | "copy-webpack-plugin": "^11.0.0", 26 | "eslint": "^8.16.0", 27 | "eslint-config-standard": "^17.0.0", 28 | "eslint-plugin-import": "^2.26.0", 29 | "eslint-plugin-node": "^11.1.0", 30 | "eslint-plugin-promise": "^6.0.0", 31 | "eslint-plugin-standard": "^5.0.0", 32 | "webpack": "^5.72.1", 33 | "webpack-cli": "^4.9.2" 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /snapshot.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/justintien/docsify-plugin-toc/c0ef227a9d2a4c1ca93b183c76f73d268009725d/snapshot.png -------------------------------------------------------------------------------- /src/assets/light.css: -------------------------------------------------------------------------------- 1 | @media only screen and (max-width: 1299px) { 2 | aside.toc-nav { 3 | max-width: 45px; 4 | opacity: 0.6; 5 | right: 0 !important; 6 | margin-right: 0 !important; 7 | background: #fff; 8 | } 9 | aside.toc-nav:hover { 10 | max-width: inherit; 11 | opacity: 1; 12 | } 13 | } 14 | 15 | @media only screen and (min-width: 1300px) { 16 | section.content { 17 | padding-right: 250px; 18 | } 19 | } 20 | 21 | aside.toc-nav { 22 | position: fixed; 23 | top: 70px; 24 | right: 3%; 25 | margin-right: 20px; 26 | width: 250px; 27 | z-index: 999999; 28 | align-self: flex-start; 29 | flex: 0 0 auto; 30 | overflow-y: auto; 31 | max-height: 70%; 32 | } 33 | 34 | aside.toc-nav.nothing { 35 | width: 0; 36 | } 37 | 38 | .page_toc { 39 | position: relative; 40 | left: 0; 41 | margin: 10px 0; 42 | border: none; 43 | font-size: 1.0em; 44 | } 45 | 46 | .page_toc p.title { 47 | margin: 0; 48 | padding-bottom: 5px; 49 | font-weight: 600; 50 | font-size: 1.2em; 51 | } 52 | 53 | .page_toc .anchor:hover:after { 54 | content: ""; 55 | } 56 | .page_toc div[class^="lv"] a:hover span { 57 | color: var(--sidebar-nav-link-color--active, #42b983); 58 | } 59 | 60 | .page_toc div { 61 | border-left: 2px solid #e8e8e8; 62 | text-indent: 10px; 63 | padding: 2px 0; 64 | cursor: pointer; 65 | } 66 | 67 | .page_toc div.active { 68 | border-left-color: var(--sidebar-nav-link-color--active, #42b983); 69 | transition: border-left-color 0.23s; 70 | } 71 | 72 | .page_toc div.active a span { 73 | color: var(--sidebar-nav-link-color--active, #42b983); 74 | transition: color 0.23s; 75 | } 76 | 77 | .page_toc div[class^="lv"] a { 78 | color: var(--text-color-base, black); 79 | text-decoration: none; 80 | font-weight: 300; 81 | line-height: 2em; 82 | display: block; 83 | } 84 | 85 | .page_toc div[class^="lv"] a span { 86 | color: var(--sidebar-nav-link-color--hover, var(--sidebar-nav-link-color)); 87 | display: block; 88 | overflow: hidden; 89 | white-space: nowrap; 90 | text-overflow:ellipsis; 91 | } 92 | 93 | .page_toc div.lv2 { 94 | text-indent: 20px; 95 | } 96 | 97 | .page_toc div.lv3 { 98 | text-indent: 30px; 99 | } 100 | 101 | .page_toc div.lv4 { 102 | text-indent: 40px; 103 | } 104 | 105 | .page_toc div.lv5 { 106 | text-indent: 50px; 107 | } 108 | 109 | .page_toc div.lv6 { 110 | text-indent: 60px; 111 | } -------------------------------------------------------------------------------- /src/index.js: -------------------------------------------------------------------------------- 1 | import { install } from './toc' 2 | 3 | if (!window.$docsify) { 4 | window.$docsify = {} 5 | } 6 | 7 | window.$docsify.plugins = (window.$docsify.plugins || []).concat(install) 8 | -------------------------------------------------------------------------------- /src/toc.js: -------------------------------------------------------------------------------- 1 | // To collect headings and then add to the page ToC 2 | function pageToC (headings, path) { 3 | let toc = ['
'] 4 | const list = [] 5 | const ignoreHeaders = window.$docsify.toc.ignoreHeaders || [] 6 | headings = document.querySelectorAll(`#main ${window.$docsify.toc.target}`) 7 | 8 | if (headings) { 9 | headings.forEach(function (heading) { 10 | const innerText = heading.innerText 11 | const innerHtml = heading.innerHTML 12 | 13 | let needSkip = false 14 | if (ignoreHeaders.length > 0) { 15 | console.error(innerText) 16 | needSkip = ignoreHeaders.some(str => innerText.match(str)) 17 | } 18 | 19 | if (needSkip) return 20 | 21 | const item = generateToC(heading.tagName.replace(/h/gi, ''), innerHtml) 22 | if (item) { 23 | list.push(item) 24 | } 25 | }) 26 | } 27 | if (list.length > 0) { 28 | toc = toc.concat(list) 29 | toc.push('
') 30 | return toc.join('') 31 | } else { 32 | return '' 33 | } 34 | } 35 | 36 | // To generate each ToC item 37 | function generateToC (level, html) { 38 | if (level >= 1 && level <= window.$docsify.toc.tocMaxLevel) { 39 | const heading = ['
', html, '
'].join('') 40 | return heading 41 | } 42 | return '' 43 | } 44 | 45 | // scroll listener 46 | const scrollHandler = () => { 47 | const clientHeight = window.innerHeight 48 | const titleBlocks = document.querySelectorAll(`#main ${window.$docsify.toc.target}`) 49 | let insightBlocks = [] 50 | titleBlocks.forEach((titleBlock, index) => { 51 | const rect = titleBlock.getBoundingClientRect() 52 | // still in sight 53 | if (rect.top <= clientHeight && rect.height + rect.top > 0) { 54 | insightBlocks.push(index) 55 | } 56 | }) 57 | const scrollingElement = document.scrollingElement || document.body 58 | // scroll to top, choose the first one 59 | if (scrollingElement.scrollTop === 0) { 60 | insightBlocks = [0] 61 | } else if (scrollingElement.offsetHeight - window.innerHeight - scrollingElement.scrollTop < 5 && 62 | insightBlocks.length > 0) { 63 | // scroll to bottom and still multi title in sight, choose the first one 64 | insightBlocks = [insightBlocks[0]] 65 | } 66 | if (insightBlocks.length) { 67 | const tocList = document.querySelectorAll('.page_toc>div') 68 | tocList.forEach((t, index) => { 69 | if (index === insightBlocks[0]) { 70 | t.classList.add('active') 71 | } else { 72 | t.classList.remove('active') 73 | } 74 | }) 75 | } 76 | } 77 | 78 | export function install (hook, vm) { 79 | hook.mounted(function () { 80 | const content = window.Docsify.dom.find('.content') 81 | if (content) { 82 | const nav = window.Docsify.dom.create('aside', '') 83 | window.Docsify.dom.toggleClass(nav, 'add', 'toc-nav') 84 | window.Docsify.dom.before(content, nav) 85 | } 86 | }) 87 | hook.doneEach(function () { 88 | const nav = window.Docsify.dom.find('.toc-nav') 89 | if (nav) { 90 | nav.innerHTML = pageToC().trim() 91 | if (nav.innerHTML === '') { 92 | window.Docsify.dom.toggleClass(nav, 'add', 'nothing') 93 | window.document.removeEventListener('scroll', scrollHandler) 94 | } else { 95 | window.Docsify.dom.toggleClass(nav, 'remove', 'nothing') 96 | scrollHandler() 97 | window.document.addEventListener('scroll', scrollHandler) 98 | } 99 | } 100 | }) 101 | } 102 | -------------------------------------------------------------------------------- /webpack.config.js: -------------------------------------------------------------------------------- 1 | const path = require('path') 2 | const TerserWebpackPlugin = require('terser-webpack-plugin') 3 | const CopyPlugin = require('copy-webpack-plugin') 4 | 5 | const pluginName = 'docsify-plugin-toc' 6 | 7 | module.exports = { 8 | mode: 'production', 9 | entry: { 10 | [pluginName]: [path.join(process.cwd(), 'src', 'index.js')], 11 | [pluginName + '.min']: [path.join(process.cwd(), 'src', 'index.js')] 12 | }, 13 | output: { 14 | path: path.join(process.cwd(), 'dist'), 15 | filename: '[name].js', 16 | library: 'DocsifyPluginToc', 17 | libraryTarget: 'umd', 18 | libraryExport: 'default', 19 | sourceMapFilename: '[file].map', 20 | globalObject: 'this' 21 | }, 22 | optimization: { 23 | minimizer: [ 24 | new TerserWebpackPlugin({ 25 | include: /\.min\.js$/, 26 | parallel: true, 27 | extractComments: false, 28 | terserOptions: { 29 | format: { 30 | comments: false 31 | }, 32 | compress: true, 33 | ie8: false, 34 | ecma: 5, 35 | warnings: false 36 | } 37 | }) 38 | ] 39 | }, 40 | plugins: [ 41 | new CopyPlugin({ 42 | patterns: [ 43 | { 44 | from: path.join(process.cwd(), 'src', 'assets', 'light.css'), 45 | to: path.join(process.cwd(), 'dist', 'light.css') 46 | } 47 | // { 48 | // from: path.join(process.cwd(), 'src', 'assets', 'dark.css'), 49 | // to: path.join(process.cwd(), 'dist', 'dark.css') 50 | // } 51 | ] 52 | }) 53 | ], 54 | module: {} 55 | } 56 | --------------------------------------------------------------------------------