├── .editorconfig ├── .eslintrc ├── .github └── workflows │ ├── doc-generation.yml │ └── node.js.yml ├── .gitignore ├── Changelog.md ├── Readme.md ├── bin ├── documentor.js └── init.js ├── docs ├── 0_Introduction.md ├── 1_Installation.md ├── 2_Usage │ ├── 0_CommandLine.md │ ├── 1_MarkdownFiles.md │ └── index.md ├── 3_Configuration │ ├── ExternalLibrary.md │ └── index.md ├── 9_Customization.md ├── _assets │ ├── icon.png │ ├── logo.png │ └── logofull.png └── _config.yml ├── package-lock.json ├── package.json ├── src ├── Documentor.js ├── Page.js ├── generators │ ├── HtmlGenerator.js │ └── index.js ├── helpers.js ├── index.js └── parsers │ ├── MarkdownParser.js │ └── index.js ├── templates ├── .eslintrc └── alchemy │ ├── base.html │ ├── main.js │ └── style.css └── test └── helpers.js /.editorconfig: -------------------------------------------------------------------------------- 1 | root = true 2 | 3 | [*] 4 | charset = utf-8 5 | indent_style = space 6 | indent_size = 2 7 | end_of_line = lf 8 | insert_final_newline = true 9 | trim_trailing_whitespace = true 10 | 11 | [*.md] 12 | max_line_length = 0 13 | trim_trailing_whitespace = false 14 | -------------------------------------------------------------------------------- /.eslintrc: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "airbnb-base" 3 | } -------------------------------------------------------------------------------- /.github/workflows/doc-generation.yml: -------------------------------------------------------------------------------- 1 | name: Documentation generation 2 | 3 | on: 4 | push: 5 | branches: [ master ] 6 | 7 | jobs: 8 | publish: 9 | runs-on: ubuntu-latest 10 | steps: 11 | - 12 | name: Checkout 13 | uses: actions/checkout@v2 14 | - 15 | name: Install and Build 🔧 16 | run: | 17 | npm ci 18 | npm run build:doc 19 | - 20 | name: Deploy to GitHub Pages 🚀 21 | if: success() 22 | uses: crazy-max/ghaction-github-pages@v2 23 | with: 24 | target_branch: gh-pages 25 | build_dir: docs 26 | env: 27 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 28 | -------------------------------------------------------------------------------- /.github/workflows/node.js.yml: -------------------------------------------------------------------------------- 1 | name: Node.js CI 2 | 3 | on: 4 | push: 5 | branches: [ master ] 6 | pull_request: 7 | branches: [ master ] 8 | 9 | jobs: 10 | quality: 11 | runs-on: ubuntu-latest 12 | 13 | steps: 14 | - uses: actions/checkout@v2 15 | - name: Use Node.js 14.x 16 | uses: actions/setup-node@v1 17 | with: 18 | node-version: 14.x 19 | - run: npm ci 20 | - run: npm run style 21 | 22 | test: 23 | runs-on: ubuntu-latest 24 | 25 | strategy: 26 | matrix: 27 | node-version: [10.x, 12.x, 14.x] 28 | 29 | steps: 30 | - uses: actions/checkout@v2 31 | - name: Use Node.js ${{ matrix.node-version }} 32 | uses: actions/setup-node@v1 33 | with: 34 | node-version: ${{ matrix.node-version }} 35 | - run: npm ci 36 | - run: npm test 37 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | __* 3 | -------------------------------------------------------------------------------- /Changelog.md: -------------------------------------------------------------------------------- 1 | # Changelog 2 | All notable changes to this project will be documented in this file. 3 | 4 | The format is based on [Keep a Changelog](http://keepachangelog.com/en/1.0.0/) 5 | and this project adheres to [Semantic Versioning](http://semver.org/spec/v2.0.0.html). 6 | 7 | ## [Unreleased] 8 | ### Changed 9 | - Update all dependencies to last versions 10 | 11 | ## [0.1.3] - 2018-09-19 12 | ### Fixed 13 | - Dark theme background 14 | 15 | ### Changed 16 | - Better sidebar highlight 17 | 18 | ## [0.1.2] - 2018-09-03 19 | ### Added 20 | - You can define parser option in the configuration file 21 | 22 | ### Fixed 23 | - Potential loop if the output file was in the watched folder 24 | 25 | ## [0.1.1] - 2018-09-02 26 | ### Changed 27 | - Publish v0.1.0 under v0.1.1 (v0.1.0 was already published due to a mistake) 28 | 29 | ## [0.1.0] - 2018-09-02 30 | ### Added 31 | - Table style 32 | 33 | ### Fixed 34 | - Parsing was breaking with multiple delimiters 35 | 36 | ### Changed 37 | - Create a parsers folder and refactored the parsing 38 | - Reads the version from package.json 39 | - Upgrade packages 40 | - Switch unit tests to ava 41 | -------------------------------------------------------------------------------- /Readme.md: -------------------------------------------------------------------------------- 1 | 3 | 4 | > A super intuitive doc generator from Markdown files 5 | 6 | # Installation 7 | 8 | ```sh 9 | npm -g i documentor 10 | ``` 11 | 12 | or for yarn users: `yarn global add documentor` 13 | 14 | ## Quick Usage 15 | 16 | ```sh 17 | documentor init # initialisation of the documentation 18 | ``` 19 | 20 | ```sh 21 | documentor ./docs-folder -o output.html # render the documentation to output.html 22 | ``` 23 | 24 | ![https://i.imgur.com/whek9Zm.png](https://i.imgur.com/whek9Zm.png) 25 | 26 | # [Documentation](http://bafs.github.io/Documentor) 27 | 28 | Please check the [**documentation**](http://bafs.github.io/Documentor) for more details. 29 | 30 | ## Command Line Usage 31 | 32 | - **`-i`**, **`--input`**: Input folder (optional flag) 33 | - **`-o`**, **`--output`**: Write in file 34 | - **`-t`**, **`--to`**: Output format 35 | - **`-c`**, **`--config`**: Configuration file 36 | - **`-w`**, **`--watch`**: Watch docs files with partial generation 37 | - **`-q`**, **`--quite`**: Do not output any message 38 | - **`-v`**, **`--verbose`**: Increase the verbosity 39 | - **`--var`**, **`--variable`**: Set or override config variable(s) 40 | - **`-h`**, **`--help`**: Show help 41 | 42 | ### Examples 43 | 44 | Generate `project.html` from `./docs` folder 45 | 46 | ```sh 47 | documentor ./docs -o out.html 48 | ``` 49 | 50 | Output html to STDOUT from `./docs` folder and read the configuration file `conf.yml` 51 | 52 | ```sh 53 | documentor docs -c conf.yml 54 | ``` 55 | 56 | Generate "out.html" with a custom name and footer 57 | 58 | ```sh 59 | documentor ./docs -o out.html --var.name "My Project" --var.footer "(c) Project 1.0" 60 | ``` 61 | 62 | Watch the "docs" folder and regenerate "out.html" on change 63 | 64 | ```sh 65 | documentor docs -o out.html -w 66 | ``` 67 | 68 | ## Dev 69 | 70 | ```sh 71 | npm i 72 | ``` 73 | 74 | You can run the CLI version with `node bin/documentor.js`, for example `node bin/documentor.js ./docs -o out.html`. 75 | 76 | ### Test 77 | 78 | ```sh 79 | npm test 80 | ``` 81 | 82 | # Screenshot 83 | 84 |

85 | 86 | #### TODO 87 | 88 | - [ ] Embed images from markdown 89 | - [ ] Add processing indicator 90 | -------------------------------------------------------------------------------- /bin/documentor.js: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env node 2 | /* eslint no-console: 0 */ 3 | 4 | const fs = require('fs'); 5 | const yaml = require('js-yaml'); 6 | const yargs = require('yargs'); 7 | const Documentor = require('../src'); 8 | const Init = require('./init'); 9 | 10 | console.time('time generation'); 11 | 12 | const { argv } = yargs 13 | .usage('Usage: $0 [options]') 14 | .example('$0 docs -o project.html', 'Generate "project.html" from "docs/" folder') 15 | .example('$0 docs -c conf.yml', 'Output html to STDOUT from "docs/" folder and read the configuration file "conf.yml"') 16 | .example('$0 docs -o out.html -w', 'Watch the "docs/" folder and regenerate "out.html" on change') 17 | .example('$0 docs -o out.html --var.name "My Project"', 'Generate "out.html" with custom variables') 18 | .example('$0 init', 'Interactive way to initialize your documentation') 19 | .alias('i', 'input') 20 | .nargs('i', 1) 21 | .describe('i', 'Input folder (optional flag)') 22 | .alias('o', 'output') 23 | .nargs('o', 1) 24 | .describe('o', 'Write in file') 25 | // .alias('f', 'from') 26 | // .nargs('f', 1) 27 | // .describe('f', 'Input format') 28 | .alias('t', 'to') 29 | .nargs('t', 1) 30 | .describe('t', 'Output format') 31 | .recommendCommands() 32 | .alias('c', 'config') 33 | .nargs('c', 1) 34 | .describe('c', 'Configuration file') 35 | .alias('w', 'watch') 36 | .nargs('w', 0) 37 | .describe('w', 'Watch docs files with partial generation') 38 | .option('config') 39 | .alias('var', 'variable') 40 | .describe('var', 'Set or override config variable(s)') 41 | .alias('q', 'quite') 42 | .nargs('q', 0) 43 | .describe('q', 'Do not output any message') 44 | .alias('v', 'verbose') 45 | .nargs('v', 0) 46 | .describe('v', 'Increase the verbosity') 47 | .help('h') 48 | .alias('h', 'help'); 49 | 50 | if (!argv.input) { 51 | argv.input = argv._.shift(); 52 | } 53 | 54 | if (!argv.input) { 55 | yargs.showHelp(); 56 | process.exit(); 57 | } 58 | 59 | if (argv.input === 'init') { 60 | Init(); 61 | } else { 62 | const confFile = argv.config || `${argv.input}/_config.yml`; 63 | let config = {}; 64 | 65 | if (fs.existsSync(confFile)) { 66 | try { 67 | config = yaml.safeLoad(fs.readFileSync(confFile, 'utf8')); 68 | } catch (e) { 69 | console.error(e); 70 | } 71 | } else if (argv.config) { 72 | console.log(`Configuration file "${confFile}" does not exists`); 73 | } 74 | 75 | if (argv.var) { 76 | // Override the config with user variables 77 | config = { 78 | ...config, 79 | ...argv.var, 80 | }; 81 | } 82 | 83 | const { to } = argv; 84 | config.format = { 85 | // from, 86 | to, 87 | }; 88 | 89 | const documentor = new Documentor(argv.input, config); 90 | 91 | if (argv.watch) { 92 | console.log(`Watch files in '${argv.input}'`); 93 | documentor.watch(argv.output, (type, pathname) => { 94 | if (pathname) { 95 | console.log(`[${type}] ${pathname}`); 96 | } else { 97 | console.log(type); 98 | } 99 | }); 100 | } else { 101 | const res = documentor.generate(argv.output, argv.quite ? () => {} : console.info); 102 | if (!argv.quite && argv.verbose) { 103 | res.then(() => console.timeEnd('time generation')); 104 | } 105 | } 106 | } 107 | -------------------------------------------------------------------------------- /bin/init.js: -------------------------------------------------------------------------------- 1 | /* eslint no-console: 0 */ 2 | 3 | const fs = require('fs'); 4 | const yaml = require('js-yaml'); 5 | const inquirer = require('inquirer'); 6 | 7 | const questions = [ 8 | { 9 | type: 'input', 10 | name: 'name', 11 | message: 'Name of your documentation', 12 | }, 13 | { 14 | type: 'input', 15 | name: 'version', 16 | message: 'Documentation version', 17 | default: '1.0.0', 18 | }, 19 | ]; 20 | 21 | const contentFirstFile = `## Welcome to your brand new documentation 22 | 23 | To render this documentation (in index.html) run 24 | 25 | \`\`\`bash 26 | documentor ./{dir} -o index.html 27 | \`\`\` 28 | `; 29 | 30 | module.exports = () => { 31 | inquirer.prompt(questions) 32 | .then((ans) => { 33 | const dirName = ans.name.replace(' ', '_'); 34 | 35 | if (fs.existsSync(dirName)) { 36 | throw new Error(`Directory '${dirName}' already exists`); 37 | } 38 | fs.mkdirSync(dirName); 39 | 40 | fs.writeFileSync(`${dirName}/_config.yml`, yaml.safeDump(ans)); 41 | fs.writeFileSync(`${dirName}/Introduction.md`, contentFirstFile.replace('{dir}', dirName)); 42 | 43 | console.log(`> You can now run 'documentor ./${dirName} -o index.html' to render your documentation.`); 44 | }); 45 | }; 46 | -------------------------------------------------------------------------------- /docs/0_Introduction.md: -------------------------------------------------------------------------------- 1 | # Welcome to Documentor documentation 2 | 3 | > A super intuitive doc generator from Markdown files 4 | 5 | Documentor was made to create a documentation from simple flat markdown files. 6 | 7 | The goal of the project is 8 | 9 | - To have an easy way to do it 10 | - To embed everything (style, script, images) in a single html file 11 | - To be able to read the documentation without javascript 12 | - To have a simple search engine (if javascript is enabled) 13 | - To be easily customizable 14 | -------------------------------------------------------------------------------- /docs/1_Installation.md: -------------------------------------------------------------------------------- 1 | Install Documentor globally to have the `documentor` command in your terminal. 2 | 3 | Please make sure that you have node version 7.6.0+. 4 | 5 | ```bash 6 | # for npm users 7 | npm install -g documentor 8 | # for yarn users 9 | yarn global add documentor 10 | ``` 11 | -------------------------------------------------------------------------------- /docs/2_Usage/0_CommandLine.md: -------------------------------------------------------------------------------- 1 | - **`-i`**, **`--input`**: Input folder (optional flag) 2 | - **`-o`**, **`--output`**: Write in file 3 | - **`-t`**, **`--to`**: Output format 4 | - **`-c`**, **`--config`**: Configuration file 5 | - **`-w`**, **`--watch`**: Watch docs files with partial generation 6 | - **`-q`**, **`--quite`**: Do not output any message 7 | - **`-v`**, **`--verbose`**: Increase the verbosity 8 | - **`--var`**, **`--variable`**: Set or override config variable(s) 9 | - **`-h`**, **`--help`**: Show help 10 | 11 | ### Examples 12 | 13 | Generate `project.html` from `./docs` folder 14 | 15 | ```bash 16 | documentor ./docs -o out.html 17 | ``` 18 | 19 | Output html to STDOUT from `./docs` folder and read the [configuration](#3_Configuration) file `conf.yml` 20 | 21 | ```bash 22 | documentor docs -c conf.yml 23 | ``` 24 | 25 | Generate "out.html" with a custom name and footer 26 | 27 | ```bash 28 | documentor ./docs -o out.html --var.name "My Project" --var.footer "(c) Project 1.0" 29 | ``` 30 | 31 | Watch the "docs" folder and regenerate "out.html" on change 32 | 33 | ```bash 34 | documentor docs -o out.html -w 35 | ``` 36 | -------------------------------------------------------------------------------- /docs/2_Usage/1_MarkdownFiles.md: -------------------------------------------------------------------------------- 1 | # File Structure 2 | 3 | Documentor will list all markdown file with the .md or .markdown extension to create pages of the documentation. 4 | 5 | To *hide* a file from the listing, you can prefix the file with an underscore (eg. `_hideMe.md`). 6 | 7 | The pages are *sorted* alphabetically. To sort your pages manually you can prefix a number followed by an underscore (eg. `0_Introduction`, `1_HowToUse`, etc.). 8 | 9 | It is possible to have sub pages by creating sub files in a folder. The folder name will be seen as an empty page. To add content to this page, you can name your file `index.md`. 10 | 11 | The title of the page is generated from the file name. The file name will be decamelized (eg. *MySuperTitle* will become *My Super Title*). 12 | 13 | ### Example 14 | 15 | ```bash 16 | . 17 | └── docs 18 | ├── _config.yml # Optional configuration 19 |    ├── 0_Introduction.md 20 | ├── _todo.md # This will not be listed in the documentation 21 |    └── Basics 22 | ├── index.md # This will be seen as the "Basics" page 23 | ├── 0_Action.md 24 | └── 1_Reaction.md 25 | ``` 26 | 27 | 28 | # Markdown Files 29 | 30 | Markdown support the [CommonMark spec](https://spec.commonmark.org/) and table (GFM). 31 | 32 | ## Header 33 | 34 | An optional header can be specified to override predefined variables. 35 | 36 | The header must be the on top on the files, separated by `---`, in a [yaml](http://yaml.org/) format. 37 | 38 | ### Available variables 39 | 40 | | Variable | Default | Description | 41 | |------------|--------------------------------------|----------------------------------------------| 42 | | `title` | File name, decamelized with caps | Set a specific title | 43 | | `slug` | File name | To define the slug in the url (after the `#`) | 44 | 45 | ### Example 46 | 47 | ```md 48 | --- 49 | title: Introduction 50 | slug: intro 51 | --- 52 | 53 | Lorem ipsum... 54 | ``` 55 | -------------------------------------------------------------------------------- /docs/2_Usage/index.md: -------------------------------------------------------------------------------- 1 | ## Express Usage 2 | 3 | - Run `documentor init` to initialize your documentation 4 | - Run `documentor ./yourFolder -o output.html` to render your documentation 5 | 6 | ## For more Information 7 | 8 | - [Command Line Usage](#2_Usage/0_CommandLine) 9 | - [Markdown Files](#2_Usage/1_MarkdownFiles) 10 | -------------------------------------------------------------------------------- /docs/3_Configuration/ExternalLibrary.md: -------------------------------------------------------------------------------- 1 | External libraries can be loaded easily from the configuration (or a custom template), here is some 2 | examples using CDNs. 3 | 4 | 5 | # Syntax Highlighting 6 | 7 | You can add code coloration for code blocks with javascript libraries. 8 | 9 | Use the configuration `htmlHeader` to add the CSS in the header and `htmlBody` to run the javascript in the body. 10 | 11 | ### Example with Highlightjs 12 | 13 | For example with [highlightjs](https://highlightjs.org/) you can simply add the css and javascript 14 | from their CDN. 15 | 16 | ```yaml 17 | # In `_config.yml` 18 | htmlHeader: 19 | - 20 | htmlBody: 21 | - 22 | - 23 | ``` 24 | 25 | # Math Support 26 | 27 | There is many libraries to add math support, like [KaTeX](https://github.com/KaTeX/KaTeX) and [MathJax](https://www.mathjax.org/). 28 | 29 | ### Example with KaTeX 30 | 31 | Follow [the documentation](https://github.com/KaTeX/KaTeX) and add the right css/javascript from 32 | their CDN. 33 | 34 | With the basic configuration, you will be able to type latex formulas between `$$`. 35 | 36 | Example: `$$c = \\pm\\sqrt{a^2 + b^2}$$` 37 | 38 | ```yaml 39 | # In `_config.yml` 40 | htmlHeader: 41 | - 42 | htmlBody: 43 | - 44 | - 46 | ``` 47 | 48 | > Note: The library is not loaded in this documentation 49 | 50 | 51 | # Diagrams Support 52 | 53 | ### Example with Mermaid 54 | 55 | You can use [Mermaid](https://github.com/mermaid-js/mermaid) to create diagrams from text, with this 56 | example you need to add the "mermaid" language on code blocks. 57 | 58 | ``` 59 | ```mermaid 60 | graph LR 61 | A --- B 62 | B-->C[fa:fa-ban forbidden] 63 | B-->D(fa:fa-spinner); 64 | ``​` 65 | ``` 66 | 67 | ```yaml 68 | # In `_config.yml` 69 | htmlBody: 70 | - 71 | - 72 | ``` 73 | 74 | > Note: The library is not loaded in this documentation 75 | -------------------------------------------------------------------------------- /docs/3_Configuration/index.md: -------------------------------------------------------------------------------- 1 | By default, Documentor will read `_config.yml` in the root folder of the documentation but you can pass `-c` or `--config` to use your own config path (see the [Command Line Usage](#2_Usage/0_CommandLine) for more information). 2 | 3 | # Options 4 | 5 | - **`name`** – Name of the project. It will be the main title for the html page. 6 | - **`version`** – Version of the project. 7 | - **`homepageUrl`** – Homepage of the project 8 | - **`logo`** – Main logo of the project. 9 | - **`icon`** – Icon of the project, will typically be used for the favicon of the html page. 10 | - **`footer`** – The content of the footer. 11 | - **`template`** – By default Documentor uses the *alchemy* template. To use a custom template path, start with `./` for a relative path or `/` for an absolute path. 12 | - *Example* – `template: ./mytemplate` 13 | - **`htmlHeader`** – List of html element to add in the header 14 | - **`htmlBody`** – List of html element to add in the body 15 | - **`markdown-it`** – You can specified some options to the parser. Please read [Markdown-it doc](https://github.com/markdown-it/markdown-it#init-with-presets-and-options) for 16 | more info. 17 | 18 | All fields are optionals. 19 | 20 | ## Example of a Configuration File 21 | 22 | ```js 23 | name: Documentor 24 | version: 1.0.0 25 | logo: _assets/logo.png 26 | icon: _assets/icon.png 27 | ``` 28 | -------------------------------------------------------------------------------- /docs/9_Customization.md: -------------------------------------------------------------------------------- 1 | You can customize the style or structure by creating your own template. 2 | 3 | ```bash 4 | documentor ./docs -o out.html --var.template "./myTemplate" 5 | ``` 6 | 7 | A template is composed of html, javascript and css. The structure of the template needs to be the following: 8 | 9 | ``` 10 | . 11 | └── myTemplate 12 |    ├── base.html 13 |    ├── main.js 14 |    └── style.css 15 | ``` 16 | 17 | The javascript and css files are optional. To begin, check how the build-in template is made in the [templates/alchemy](https://github.com/BafS/Documentor/tree/master/templates/alchemy) folder. 18 | 19 | ## Html 20 | 21 | The html use [Handlebar](http://handlebarsjs.com/) as a template, Handlebars is largely compatible with Mustache templates. 22 | 23 | ## Style (css) 24 | 25 | Documentor uses Postcss with [cssnext](http://cssnext.io/). You can thus uses the [features](http://cssnext.io/features/) of cssnext. 26 | 27 | ## Javascript 28 | 29 | Javascript is transpiled with [Babel](https://babeljs.io/) to ECMAScript 5. You can use ECMAScript 6, modules, fat arrow etc. 30 | -------------------------------------------------------------------------------- /docs/_assets/icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/BafS/Documentor/b13b709bb5133b374d41e79bb46f9d7662904df3/docs/_assets/icon.png -------------------------------------------------------------------------------- /docs/_assets/logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/BafS/Documentor/b13b709bb5133b374d41e79bb46f9d7662904df3/docs/_assets/logo.png -------------------------------------------------------------------------------- /docs/_assets/logofull.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/BafS/Documentor/b13b709bb5133b374d41e79bb46f9d7662904df3/docs/_assets/logofull.png -------------------------------------------------------------------------------- /docs/_config.yml: -------------------------------------------------------------------------------- 1 | name: Documentor 2 | version: 0.1.x 3 | sourceUrl: https://github.com/BafS/Documentor 4 | homepageUrl: https://github.com/BafS/Documentor 5 | logo: _assets/logo.png 6 | icon: _assets/icon.png 7 | # footer: Your footer 8 | # You can use a pre-made template ("alchemy" is the default one) or use a relative path to your own template 9 | # template: basic 10 | # template: ./testtemp 11 | htmlHeader: 12 | - 13 | htmlBody: 14 | - 15 | - 16 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "documentor", 3 | "version": "0.1.3", 4 | "description": "Documentation generator", 5 | "main": "src", 6 | "license": "MIT", 7 | "author": { 8 | "name": "Fabien Sa" 9 | }, 10 | "repository": "https://github.com/BafS/Documentor", 11 | "bin": { 12 | "documentor": "./bin/documentor.js" 13 | }, 14 | "scripts": { 15 | "test": "ava -v", 16 | "style": "eslint src/** bin/**", 17 | "build:doc": "node bin/documentor docs -o docs/index.html" 18 | }, 19 | "dependencies": { 20 | "@babel/core": "^7.0.0", 21 | "@babel/preset-env": "^7.0.0", 22 | "chokidar": "^3.4.0", 23 | "handlebars": "^4.0.11", 24 | "inquirer": "^7.1.0", 25 | "js-yaml": "^3.12.0", 26 | "markdown-it": "^11.0.0", 27 | "postcss-preset-env": "^6.7.0", 28 | "yargs": "^15.3.0" 29 | }, 30 | "devDependencies": { 31 | "ava": "^3.8.2", 32 | "eslint": "^7.1.0", 33 | "eslint-config-airbnb-base": "^14.1.0", 34 | "eslint-plugin-import": "^2.14.0" 35 | }, 36 | "engines": { 37 | "node": ">=7.6.0" 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /src/Documentor.js: -------------------------------------------------------------------------------- 1 | const fs = require('fs'); 2 | const chokidar = require('chokidar'); 3 | const { 4 | getBasename, 5 | getExtension, 6 | humanizesSlug, 7 | escapeRegExp, 8 | } = require('./helpers'); 9 | const generators = require('./generators'); 10 | const parsers = require('./parsers'); 11 | const packageObj = require('../package.json'); 12 | 13 | const output = (outputFile, out) => { 14 | if (!outputFile) { 15 | process.stdout.write(out); 16 | return out; 17 | } 18 | 19 | fs.writeFileSync(outputFile, out); 20 | return true; 21 | }; 22 | 23 | module.exports = class Documentor { 24 | /** 25 | * Documentor constructor 26 | * @param {string} dir 27 | * @param {{}} config 28 | */ 29 | constructor(dir = '.', config = {}) { 30 | const defaultConfig = { 31 | extensions: ['md', 'markdown'], 32 | template: 'alchemy', 33 | }; 34 | 35 | const systemConfig = { 36 | documentor_version: packageObj.version || '', 37 | }; 38 | 39 | this.dir = dir.replace(/\/+$/, '/'); 40 | this.config = { 41 | ...defaultConfig, 42 | ...config, 43 | ...systemConfig, 44 | }; 45 | } 46 | 47 | /** 48 | * Return an array of Page as a tree 49 | * @private 50 | * @param {string} pathname 51 | * @param {Page[]} [arr=[]] 52 | * @returns {Page[]} 53 | */ 54 | pagesTree(pathname, arr = []) { 55 | const dir = fs.readdirSync(pathname); 56 | const parser = parsers.markdown(this.config); 57 | dir.forEach((name) => { 58 | const target = `${pathname}/${name}`; 59 | 60 | const stats = fs.statSync(target); 61 | if (stats.isFile() && this.config.extensions.indexOf(getExtension(name)) !== -1 && name.substr(0, 1) !== '_') { 62 | // Page 63 | let pageTarget = target; 64 | if (target.indexOf(this.dir) === 0) { 65 | const re = new RegExp(`^${this.dir}\\/*`, ''); 66 | pageTarget = target.replace(re, ''); 67 | } 68 | 69 | arr.push(parser(pageTarget, fs.readFileSync(target, 'utf8'))); 70 | } else if (stats.isDirectory() && name.substr(0, 1) !== '_') { 71 | const children = this.pagesTree(target); 72 | const indexBase = children.findIndex((page) => getBasename(page.slug) === 'index'); 73 | const page = children[indexBase] || parser(name); 74 | 75 | if (children[indexBase]) { 76 | children.splice(indexBase, 1); 77 | page.slug = page.slug.substr(0, page.slug.lastIndexOf('/index')); 78 | if (page.title === 'index') { 79 | page.title = humanizesSlug(name); 80 | } 81 | } 82 | 83 | arr.push({ ...page, children }); 84 | } 85 | }); 86 | 87 | return arr; 88 | } 89 | 90 | /** 91 | * Returns the Generator object 92 | * @private 93 | * @param {string} outputFile 94 | * @returns {HtmlGenerator} 95 | */ 96 | generatorObject(outputFile) { 97 | const format = ((this.config.format || {}).to || getExtension(outputFile || '')).toLowerCase(); 98 | if (!generators[format]) { 99 | throw new Error(`Invalid output format ('${format}')`); 100 | } 101 | 102 | const generatorObj = new generators[format](this.dir, this.config); 103 | 104 | if (!generatorObj.generate) { 105 | throw new Error(`The '${format}' generator is not valid (no 'generate' method in ${generatorObj.constructor.name})`); 106 | } 107 | 108 | return generatorObj; 109 | } 110 | 111 | /** 112 | * Watch source files to do partial generation when possible 113 | * @param {string} outputFile 114 | * @param {(type: string, pathname: string, generation: Promise) => {}} callback 115 | */ 116 | async watch(outputFile, callback) { 117 | if (!outputFile) { 118 | throw new Error('Invalid output file'); 119 | } 120 | 121 | let additionalRegex = ''; 122 | if (outputFile.startsWith(this.dir)) { 123 | const fileLastPart = outputFile.substr(this.dir.length); 124 | additionalRegex = `|${escapeRegExp(fileLastPart)}`; 125 | } 126 | 127 | const watcher = chokidar.watch([this.dir], { 128 | ignored: new RegExp(`(^|[/\\\\])\\..|\\.ya?ml${additionalRegex}$`), 129 | persistent: true, 130 | ignoreInitial: true, 131 | }); 132 | 133 | const generatorObj = this.generatorObject(outputFile); 134 | const generatorFn = await generatorObj.generate(); 135 | const regenerate = () => generatorFn(this.pagesTree(this.dir)); 136 | 137 | const onChange = (type, pathname = null) => { 138 | const generation = new Promise((resolve, reject) => { 139 | const out = regenerate(); 140 | if (output(outputFile, out)) { 141 | resolve(); 142 | return; 143 | } 144 | reject(); 145 | }); 146 | 147 | callback(type, pathname, generation); 148 | }; 149 | 150 | onChange('First generation'); 151 | 152 | watcher 153 | .on('add', (pathname) => onChange('add', pathname)) 154 | .on('change', (pathname) => onChange('change', pathname)) 155 | .on('unlink', (pathname) => onChange('unlink', pathname)); 156 | } 157 | 158 | /** 159 | * Generate documentation 160 | * @param {string} outputFile 161 | * @param {(message: string) => void} log 162 | * @returns {Promise} 163 | */ 164 | async generate(outputFile = null, log = () => {}) { 165 | const generatorObj = this.generatorObject(outputFile); 166 | 167 | log(`Compile markdown files (from "${this.dir}")`); 168 | const pagesTree = this.pagesTree(this.dir); 169 | 170 | log('Generate documentation'); 171 | const out = (await generatorObj.generate())(pagesTree); 172 | 173 | log(`Save output (to "${outputFile}")`); 174 | return output(outputFile, out); 175 | } 176 | }; 177 | -------------------------------------------------------------------------------- /src/Page.js: -------------------------------------------------------------------------------- 1 | module.exports = class Page { 2 | /** 3 | * Constructor 4 | * @param {string} title 5 | * @param {string} slug 6 | * @param {string} content 7 | * @param {array} options 8 | */ 9 | constructor(title, slug, content, options = []) { 10 | this.title = title; 11 | this.slug = slug; 12 | this.content = content; 13 | this.options = options; 14 | } 15 | }; 16 | -------------------------------------------------------------------------------- /src/generators/HtmlGenerator.js: -------------------------------------------------------------------------------- 1 | const fs = require('fs'); 2 | const babel = require('@babel/core'); 3 | const Handlebars = require('handlebars'); 4 | const path = require('path'); 5 | const postcssPresetEnv = require('postcss-preset-env'); 6 | const { getExtension, readFile } = require('../helpers'); 7 | 8 | module.exports = class { 9 | /** 10 | * Constructor 11 | * @param {string} dir 12 | * @param {{}} config 13 | */ 14 | constructor(dir, config) { 15 | this.dir = dir; 16 | this.config = config; 17 | 18 | if (config.template.substr(0, 2) === './' || config.template.substr(0, 1) === '/') { 19 | this.templatePath = config.template; 20 | } else { 21 | const basePath = path.join(__dirname, '..', '..'); 22 | this.templatePath = `${basePath}/templates/${config.template}`; 23 | } 24 | 25 | if (!fs.existsSync(`${this.templatePath}/base.html`)) { 26 | throw Error(`Template '${this.templatePath}' is not a valid template folder`); 27 | } 28 | } 29 | 30 | /** 31 | * Generate javascript 32 | * @private 33 | * @returns {Promise} 34 | */ 35 | async generateJavascript() { 36 | if (fs.existsSync(`${this.templatePath}/main.js`)) { 37 | return new Promise((resolve, reject) => { 38 | babel.transformFile(`${this.templatePath}/main.js`, { 39 | minified: true, 40 | presets: ['@babel/preset-env'], 41 | }, (err, res) => { 42 | if (err) { 43 | reject(err); 44 | return; 45 | } 46 | resolve(res.code); 47 | }); 48 | }); 49 | } 50 | 51 | return false; 52 | } 53 | 54 | /** 55 | * Generate style 56 | * @private 57 | * @returns {Promise} 58 | */ 59 | async generateStyle() { 60 | const cssPath = `${this.templatePath}/style.css`; 61 | if (fs.existsSync(cssPath)) { 62 | const style = await readFile(cssPath, 'utf8'); 63 | return postcssPresetEnv.process(style, { from: cssPath }) 64 | .then((result) => result.css); 65 | } 66 | 67 | return false; 68 | } 69 | 70 | /** 71 | * Generate an inline image 72 | * @private 73 | * @param {string} imagePath 74 | * @returns {Promise} base64 image data 75 | */ 76 | async generateImage(imagePath) { 77 | if (imagePath && fs.existsSync(`${this.dir}/${imagePath}`)) { 78 | const data = await readFile(`${this.dir}/${imagePath}`); 79 | const ext = getExtension(imagePath); 80 | const base64data = Buffer.from(data).toString('base64'); 81 | return `data:image/${ext};base64,${base64data}`; 82 | } 83 | 84 | return false; 85 | } 86 | 87 | /** 88 | * Get template data 89 | * @private 90 | * @returns {{}} 91 | */ 92 | async templateData() { 93 | const [logo, icon, css, javascript] = await Promise.all([ 94 | this.generateImage(this.config.logo), 95 | this.generateImage(this.config.icon), 96 | this.generateStyle(), 97 | this.generateJavascript(), 98 | ]); 99 | 100 | return { 101 | ...this.config, 102 | logo, 103 | icon, 104 | css, 105 | javascript, 106 | }; 107 | } 108 | 109 | /** 110 | * Html generator 111 | * @returns {Promise<(pages: Page[]) => string>} 112 | */ 113 | async generate() { 114 | const templateRaw = await readFile(`${this.templatePath}/base.html`, 'utf8'); 115 | const template = Handlebars.compile(templateRaw); 116 | const data = await this.templateData(); 117 | 118 | return (pages) => template({ ...data, pages }); 119 | } 120 | }; 121 | -------------------------------------------------------------------------------- /src/generators/index.js: -------------------------------------------------------------------------------- 1 | const HtmlGenerator = require('./HtmlGenerator'); 2 | 3 | module.exports = { 4 | html: HtmlGenerator, 5 | }; 6 | -------------------------------------------------------------------------------- /src/helpers.js: -------------------------------------------------------------------------------- 1 | const fs = require('fs'); 2 | const path = require('path'); 3 | 4 | const helpers = { 5 | strToSlug: (str) => ( 6 | str 7 | .replace(/^\.\//, '') 8 | .replace(/\.[^/.]+$/, '') 9 | .replace(/ /g, '') 10 | ), 11 | 12 | removeLeadingNumber: (slug) => slug.replace(/^[0-9]+_(.+)/, '$1'), 13 | 14 | humanizesSlug: (name) => ( 15 | helpers 16 | .removeLeadingNumber(name) 17 | .replace(/_/g, ' ') 18 | .trim() 19 | .replace(/([a-z])([A-Z])/g, (match, p1, p2) => `${p1} ${p2}`) 20 | ), 21 | 22 | getExtension: (filename) => path.extname(filename).substr(1), 23 | 24 | getBasename: (filename) => path.basename(filename, path.extname(filename)), 25 | 26 | exists: async (filePath) => ( 27 | new Promise((resolve, reject) => { 28 | fs.stat(filePath, (err, stats) => { 29 | if (err) { 30 | reject(err); 31 | return; 32 | } 33 | resolve(stats); 34 | }); 35 | }) 36 | ), 37 | 38 | readFile: async (filePath, options) => ( 39 | new Promise((resolve, reject) => { 40 | fs.readFile(filePath, options, (err, data) => { 41 | if (err) { 42 | reject(err); 43 | return; 44 | } 45 | resolve(data); 46 | }); 47 | }) 48 | ), 49 | 50 | escapeRegExp: (str) => ( 51 | // https://github.com/sindresorhus/escape-string-regexp 52 | str.replace(/[|\\{}()[\]^$+*?.]/g, '\\$&') 53 | ), 54 | }; 55 | 56 | module.exports = helpers; 57 | -------------------------------------------------------------------------------- /src/index.js: -------------------------------------------------------------------------------- 1 | module.exports = require('./Documentor'); 2 | -------------------------------------------------------------------------------- /src/parsers/MarkdownParser.js: -------------------------------------------------------------------------------- 1 | const yaml = require('js-yaml'); 2 | const MarkdownIt = require('markdown-it'); 3 | const Page = require('../Page'); 4 | const { humanizesSlug, getBasename, strToSlug } = require('../helpers'); 5 | 6 | module.exports = (config) => { 7 | const md = new MarkdownIt(config['markdown-it']); 8 | const delimiter = '---'; 9 | 10 | /** 11 | * Parse the content and create a page object 12 | * @param {string} filename 13 | * @param {string} [data=''] data 14 | * @returns {Page} 15 | */ 16 | return (filename, data = '') => { 17 | const slug = strToSlug(filename); 18 | 19 | // Split '---' to get the optional yaml header 20 | const parts = data.split(delimiter); 21 | 22 | // If we have a yaml header 23 | if (parts.length >= 3 && parts[0] === '') { 24 | const [, optionsYaml, ...contents] = parts; 25 | const options = yaml.safeLoad(optionsYaml); 26 | 27 | const content = md.render(contents.join(delimiter)); 28 | 29 | return new Page(options.title || optionsYaml, options.slug || slug, content, options); 30 | } 31 | 32 | const title = humanizesSlug(getBasename(filename)); 33 | 34 | return new Page(title, slug, md.render(data)); 35 | }; 36 | }; 37 | -------------------------------------------------------------------------------- /src/parsers/index.js: -------------------------------------------------------------------------------- 1 | const MarkdownParser = require('./MarkdownParser'); 2 | 3 | module.exports = { 4 | markdown: MarkdownParser, 5 | }; 6 | -------------------------------------------------------------------------------- /templates/.eslintrc: -------------------------------------------------------------------------------- 1 | { 2 | "env": { 3 | "browser": true 4 | } 5 | } 6 | -------------------------------------------------------------------------------- /templates/alchemy/base.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | {{name}} 9 | {{#if icon}} 10 | 11 | {{/if}} 12 | 15 | {{#each htmlHeader}} 16 | {{{this}}} 17 | {{/each}} 18 | 19 | 20 |
21 | 57 | 58 |
59 |
60 | 61 | 64 | 65 | 66 |
67 | 68 | {{#each pages}} 69 |
70 |

{{title}}

71 | {{{content}}} 72 |
73 | 74 | {{#each children}} 75 |
76 |

{{title}}

77 | {{{content}}} 78 |
79 | {{/each}} 80 | {{/each}} 81 | 82 |
83 | {{#if footer}} 84 | {{{footer}}} 85 | {{else}} 86 | Build with Documentor {{documentor_version}} 87 | {{/if}} 88 |
89 |
90 |
91 | 94 | {{#each htmlBody}} 95 | {{{this}}} 96 | {{/each}} 97 | 98 | 99 | -------------------------------------------------------------------------------- /templates/alchemy/main.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Get the current hash (without the leading #) 3 | * @returns {string} 4 | */ 5 | const currentHash = () => window.location.hash.substr(1) || ''; 6 | 7 | /** 8 | * Get all pages element 9 | * @returns {Element[]} 10 | */ 11 | const getPageElements = () => [...document.querySelectorAll('.content .page')]; 12 | 13 | const pageEls = getPageElements(); 14 | 15 | /** 16 | * Get current page index from an hash string 17 | * @param {number} hash 18 | */ 19 | const getCurrentPageIndex = hash => pageEls.findIndex((page, i) => page.id === hash || (i === 0 && hash === '')); 20 | 21 | /** 22 | * Display class from hash string 23 | * @param {string} hash 24 | */ 25 | const showPageFromHash = (hash) => { 26 | let activeIndex = 0; 27 | pageEls.forEach((page, i) => { 28 | if (page.id === hash || (i === 0 && hash === '')) { 29 | activeIndex = i; 30 | page.classList.remove('hide'); 31 | } else { 32 | page.classList.add('hide'); 33 | } 34 | }); 35 | return activeIndex; 36 | }; 37 | 38 | /** 39 | * Toggle class for an element 40 | * @param {Element} element 41 | * @param {string} className 42 | */ 43 | const toggleClass = (element, className) => { 44 | if (element.classList !== null && element.classList.contains(className)) { 45 | element.classList.remove(className); 46 | } else { 47 | element.classList.add(className); 48 | } 49 | }; 50 | 51 | /** 52 | * Highlight the given word 53 | * @param {Element} root 54 | * @param {string} word 55 | * @param {string} [className='highlight'] className 56 | */ 57 | function highlightWord(root, word, className = 'highlight') { 58 | const excludeElements = ['script', 'style', 'iframe', 'canvas']; 59 | let found = false; 60 | 61 | /** 62 | * @returns {Node[]} 63 | */ 64 | const textNodesUnder = () => { 65 | const walk = document.createTreeWalker(root, NodeFilter.SHOW_TEXT, null, false); 66 | const text = []; 67 | 68 | while (walk.nextNode()) { 69 | if (excludeElements.indexOf(walk.currentNode.parentElement.tagName.toLowerCase()) < 0) { 70 | text.push(walk.currentNode); 71 | } 72 | } 73 | return text; 74 | }; 75 | 76 | /** 77 | * Highlight words 78 | * @param {Node} n 79 | * @returns {boolean} 80 | */ 81 | const highlightWords = (n) => { 82 | const indexOfNode = (node, i) => node.nodeValue.toLowerCase().indexOf(word.toLowerCase(), i); 83 | let after; 84 | let span; 85 | let i = indexOfNode(n); 86 | 87 | if (!found && i > -1) { 88 | found = true; 89 | } 90 | 91 | while (i > -1) { 92 | after = n.splitText(i + word.length); 93 | span = document.createElement('span'); 94 | span.className = className; 95 | span.appendChild(n.splitText(i)); 96 | after.parentNode.insertBefore(span, after); 97 | n = after; 98 | i = indexOfNode(after, i); 99 | } 100 | }; 101 | 102 | textNodesUnder().forEach(highlightWords); 103 | return found; 104 | } 105 | 106 | /** 107 | * Remove highlight from elements 108 | * @param {Element[]} elements 109 | */ 110 | const removeHighlight = (elements) => { 111 | elements.forEach((el) => { 112 | const prev = el.previousSibling; 113 | const next = el.nextSibling; 114 | prev.textContent += (el.textContent + next.textContent); 115 | el.parentNode.removeChild(next); 116 | el.parentNode.removeChild(el); 117 | }); 118 | }; 119 | 120 | const sidebarLinks = document.querySelectorAll('.sidebar ul li a'); 121 | 122 | /** 123 | * Highlight sidebar link 124 | * @param {number} index 125 | */ 126 | const addClassSidebarIndex = (index, className) => { 127 | sidebarLinks[index].classList.add(className); 128 | }; 129 | 130 | /** 131 | * Remove highlights from sidebar 132 | * @param {string} className 133 | */ 134 | const removeClassesSidebar = (className) => { 135 | sidebarLinks.forEach(link => link.classList.remove(className)); 136 | }; 137 | 138 | const toggleActiveLinkSidebar = (index) => { 139 | sidebarLinks.forEach((link, i) => { 140 | if (i === index) { 141 | link.classList.add('active'); 142 | } else { 143 | link.classList.remove('active'); 144 | } 145 | }); 146 | }; 147 | 148 | const main = document.querySelector('.main'); 149 | document.querySelector('button.toggle-sidebar').onclick = () => { 150 | toggleClass(main, 'full-width'); 151 | }; 152 | 153 | document.querySelector('button.toggle-light').onclick = () => { 154 | toggleClass(main, 'dark'); 155 | }; 156 | 157 | document.querySelector('button.next-page').onclick = () => { 158 | const nextIndex = Math.min(getCurrentPageIndex(currentHash()) + 1, pageEls.length - 1); 159 | window.location.hash = `#${pageEls[nextIndex].id}`; 160 | }; 161 | 162 | document.querySelector('button.previous-page').onclick = () => { 163 | const prevIndex = Math.max(0, (getCurrentPageIndex(currentHash()) - 1)); 164 | window.location.hash = `#${pageEls[prevIndex].id}`; 165 | }; 166 | 167 | document.querySelector('.tools').classList.remove('hide'); 168 | 169 | const searchInputEl = document.querySelector('.search-input'); 170 | searchInputEl.classList.remove('hide'); 171 | 172 | const highlightClass = 'highlight'; 173 | 174 | searchInputEl.addEventListener('keypress', (e) => { 175 | const key = e.which || e.keyCode; 176 | if (key === 13) { 177 | removeHighlight([...document.querySelectorAll('.content .highlight')]); 178 | removeClassesSidebar(highlightClass); 179 | pageEls.forEach((el, pIndex) => { 180 | if (highlightWord(el, searchInputEl.value)) { 181 | addClassSidebarIndex(pIndex, highlightClass); 182 | } 183 | }); 184 | } 185 | }); 186 | 187 | searchInputEl.addEventListener('keyup', (e) => { 188 | const key = e.which || e.keyCode; 189 | if (key !== 13 && searchInputEl.value.length < 1) { 190 | removeHighlight([...document.querySelectorAll(`.content .${highlightClass}`)]); 191 | removeClassesSidebar(highlightClass); 192 | } 193 | }); 194 | 195 | const showPageFromCurrentHash = () => { 196 | const index = showPageFromHash(currentHash()); 197 | toggleActiveLinkSidebar(index); 198 | }; 199 | 200 | if (document.body.clientWidth < 768) { 201 | toggleClass(main, 'full-width'); 202 | } 203 | 204 | // Listen for hash change 205 | window.onhashchange = () => { 206 | showPageFromCurrentHash(); 207 | }; 208 | 209 | showPageFromCurrentHash(); 210 | -------------------------------------------------------------------------------- /templates/alchemy/style.css: -------------------------------------------------------------------------------- 1 | @import url(https://fonts.googleapis.com/css?family=Lato:300,700|Merriweather:300italic,300|Inconsolata:400,700); 2 | 3 | :root { 4 | --mainColor: #373f52; 5 | --secondaryColor: #d5dae6; 6 | --mainBackgroundColor: #fff; 7 | --sidebarMainColor: #d5dae6; 8 | --sidebarBackgroundColor: #373f52; 9 | --sidebarSearchBackgroundColor: #40495f; 10 | } 11 | .dark { 12 | --mainColor: #d5dae6; 13 | --secondaryColor: #373f52; 14 | --mainBackgroundColor: #2b3140; 15 | --sidebarMainColor: #d5dae6; 16 | --sidebarBackgroundColor: #373f52; 17 | --sidebarSearchBackgroundColor: #40495f; 18 | } 19 | *, :after, :before { 20 | box-sizing: inherit; 21 | } 22 | html, body { 23 | box-sizing: border-box; 24 | height: 100%; 25 | width: 100%; 26 | } 27 | body { 28 | margin: 0; 29 | font-size: 16px; 30 | line-height: 1.6875em; 31 | font-family: Lato, sans-serif; 32 | } 33 | a { 34 | transition: all .2s linear; 35 | } 36 | hr { 37 | border: 0; 38 | margin: 30px 0; 39 | border-bottom: 1px solid #ddd; 40 | } 41 | .main { 42 | display: -ms-flexbox; 43 | display: -ms-flex; 44 | display: flex; 45 | -ms-flex-pack: end; 46 | justify-content: flex-end; 47 | min-height: 100%; 48 | background-color: var(--mainBackgroundColor); 49 | } 50 | .sidebar { 51 | display: flex; 52 | -webkit-box-orient: vertical; 53 | -moz-box-orient: vertical; 54 | -webkit-box-direction: normal; 55 | -moz-box-direction: normal; 56 | min-height: 0; 57 | flex-direction: column; 58 | width: 270px; 59 | height: 100%; 60 | position: fixed; 61 | top: 0; 62 | left: 0; 63 | z-index: 4; 64 | font-size: 15px; 65 | line-height: 18px; 66 | background: var(--sidebarBackgroundColor); 67 | color: var(--sidebarMainColor); 68 | overflow: hidden; 69 | transition: transform ease-in-out .25s; 70 | } 71 | .sidebar ul.menu { 72 | margin-bottom: 44px; 73 | overflow-y: auto; 74 | } 75 | .sidebar ul { 76 | list-style: none; 77 | margin: 0; 78 | padding: 0; 79 | } 80 | .sidebar ul > li > ul a { 81 | padding-left: 34px; 82 | height: 38px; 83 | line-height: 38px; 84 | } 85 | .sidebar ul li { 86 | margin: 0; 87 | padding: 0; 88 | line-height: 0; 89 | } 90 | .sidebar ul a { 91 | display: block; 92 | text-overflow: ellipsis; 93 | white-space: nowrap; 94 | overflow: hidden; 95 | width: 100%; 96 | color: var(--sidebarMainColor); 97 | padding: 0 14px; 98 | height: 40px; 99 | line-height: 40px; 100 | text-decoration: none; 101 | margin: 0; 102 | border-left: 2px solid transparent; 103 | } 104 | .sidebar ul a.active { 105 | background: rgba(255, 255, 255, .04); 106 | border-left: 2px solid rgba(255, 255, 255, .6); 107 | } 108 | .sidebar a:hover, .sidebar .header a:hover { 109 | color: #fff; 110 | } 111 | .sidebar ul a:hover { 112 | background: rgba(255, 255, 255, .08); 113 | } 114 | .sidebar .header a { 115 | color: var(--sidebarSecondaryColor); 116 | text-decoration: none; 117 | } 118 | .sidebar .header { 119 | padding: 0 15px; 120 | padding-bottom: 15px; 121 | border-bottom: 1px solid rgba(255, 255, 255, .5); 122 | } 123 | 124 | .sidebar .logo { 125 | margin-top: 15px; 126 | float: right; 127 | max-width: 120px; 128 | display: block; 129 | max-height: 50px; 130 | } 131 | 132 | .sidebar .search-input { 133 | font-family: Lato, sans-serif; 134 | color: var(--sidebarMainColor); 135 | background-color: var(--sidebarSearchBackgroundColor); 136 | border: 0; 137 | padding: 14px; 138 | font-size: 15px; 139 | font-weight: normal; 140 | position: absolute; 141 | bottom: 0; 142 | width: 100%; 143 | transition: box-shadow .15s ease; 144 | outline: none; 145 | } 146 | 147 | .sidebar .search-input:hover { 148 | filter: brightness(.75); 149 | } 150 | 151 | .sidebar .search-input:focus { 152 | box-shadow: 0 0 0 1px rgba(255, 255, 255, .5) inset; 153 | } 154 | 155 | .full-width .sidebar { 156 | transform: translateX(-270px); 157 | will-change: transform; 158 | } 159 | 160 | .full-width .content { 161 | margin-left: 0; 162 | } 163 | 164 | .content { 165 | font-family: Merriweather, 'Book Antiqua', Georgia, 'Century Schoolbook', serif; 166 | font-size: 1em; 167 | line-height: 1.6875em; 168 | width: 100%; 169 | margin-left: 270px; 170 | overflow-y: auto; 171 | -webkit-overflow-scrolling: touch; 172 | height: 100%; 173 | position: relative; 174 | z-index: 3; 175 | padding: 0 2rem; 176 | transition: margin ease-in-out .25s; 177 | will-change: margin-left; 178 | color: var(--mainColor); 179 | } 180 | 181 | .content h1,.content h2,.content h3,.content h4,.content h5,.content h6 { 182 | font-family: Lato, sans-serif; 183 | font-weight: 700; 184 | line-height: 1.5em; 185 | word-wrap: break-word; 186 | } 187 | 188 | .content h1 {font-size:2em; margin:1em 0 .5em} 189 | .content h1.heading {color: var(--mainColor); opacity: .8; margin: 1.25em 0 .5em} 190 | .content h1 small {font-weight:300} 191 | .content h1 a.view-source {font-size:1.2rem} 192 | .content h2 {font-size:1.6em;margin:1em 0 .5em;font-weight:700} 193 | .content h3 {font-size:1.3em; margin:1em 0 .5em; font-weight:700} 194 | .content .page a {color: var(--mainColor)} 195 | .content a *,.content a :after,.content a :before,.content a:after,.content a:before {text-shadow:none} 196 | .content a:visited {color: var(--mainColor)} 197 | .content ul li {line-height:1.5em} 198 | .content ul li>p {margin:0} 199 | .content blockquote {font-style:italic;margin:.5em 0;padding:.25em 1.5em;border-left:3px solid #e1e1e1;display:inline-block} 200 | .content blockquote :first-child {padding-top:0;margin-top:0} 201 | .content blockquote :last-child {padding-bottom:0;margin-bottom:0} 202 | .content a.no-underline,.content pre a {color:#9768d1;text-shadow:none;text-decoration:none;background-image:none} 203 | .content a.no-underline:active,.content a.no-underline:focus,.content a.no-underline:hover,.content a.no-underline:visited,.content pre a:active,.content pre a:focus,.content pre a:hover,.content pre a:visited {color:#9768d1;text-decoration:none} 204 | .content code {color: #373f52; font-weight:400;background-color:#f7f9fc;vertical-align:baseline;border-radius:2px;padding:.1em .2em;border:1px solid #d2ddee} 205 | .content pre {margin:1.5em 0; line-height: 1.5em} 206 | .content img {max-width: 100%} 207 | 208 | .content table { 209 | border-collapse: collapse; 210 | width: 100%; 211 | } 212 | 213 | .content th, .content td { 214 | padding: 0.3rem 0.5rem; 215 | text-align: left; 216 | border-bottom: 1px solid #e1e1e1; 217 | } 218 | 219 | .content .page { 220 | padding: 15px 0; 221 | transition: all .2s linear; 222 | /* min-height: 100%; */ 223 | opacity: 1; 224 | } 225 | 226 | .content .page.hide { 227 | display: none; 228 | opacity: 0; 229 | } 230 | 231 | .footer { 232 | color: #999; 233 | font-size: .8em; 234 | font-style: italic; 235 | text-align: center; 236 | margin: 30px 0; 237 | } 238 | 239 | .footer a, .footer a:visited { 240 | color: #888; 241 | } 242 | 243 | .footer a:hover { 244 | color: #444; 245 | } 246 | 247 | .tools { 248 | opacity: .6; 249 | top: 4px; 250 | left: 6px; 251 | position: absolute; 252 | transition: opacity .15s linear; 253 | } 254 | 255 | .tools:hover { 256 | opacity: .9; 257 | } 258 | 259 | .tools.hide, .search-input.hide { 260 | display: none; 261 | } 262 | 263 | .tools button { 264 | color: var(--mainColor); 265 | float: left; 266 | font-size: 16px; 267 | height: 30px; 268 | width: 30px; 269 | margin: 0; 270 | background: transparent; 271 | margin-right: 4px; 272 | font-weight: 300; 273 | border: none; 274 | line-height: 0; 275 | transition: background .2s ease; 276 | outline: none; 277 | } 278 | 279 | .tools button:hover { 280 | background: rgba(0, 0, 0, .05); 281 | cursor: pointer; 282 | } 283 | 284 | .raindrop.icon { 285 | color: var(--mainColor); 286 | position: absolute; 287 | margin-left: 3px; 288 | margin-top: -3px; 289 | width: 10px; 290 | height: 10px; 291 | border: solid 1px currentColor; 292 | border-radius: 6px 6px 6px 0; 293 | -webkit-transform: rotate(135deg); 294 | transform: rotate(135deg); 295 | } 296 | 297 | .content .highlight { 298 | background: rgb(255, 252, 53, .5); 299 | } 300 | 301 | .sidebar a.highlight:after { 302 | position: absolute; 303 | content: " "; 304 | height: 10px; 305 | width: 10px; 306 | background: rgb(255, 252, 53, .8); 307 | border-radius: 9px; 308 | right: 6%; 309 | margin-top: 14px; 310 | } 311 | 312 | @media screen and (max-width: 768px) { 313 | .content { 314 | margin-left: 0; 315 | } 316 | .tools { 317 | margin-left: 270px; 318 | } 319 | .full-width .content .tools { 320 | margin-left: 0; 321 | } 322 | .full-width .sidebar { 323 | transition: none; 324 | } 325 | } 326 | -------------------------------------------------------------------------------- /test/helpers.js: -------------------------------------------------------------------------------- 1 | const test = require('ava'); 2 | const { 3 | removeLeadingNumber, 4 | humanizesSlug, 5 | strToSlug, 6 | getBasename, 7 | getExtension, 8 | } = require('../src/helpers'); 9 | 10 | test('test removeLeadingNumber function', (t) => { 11 | // it should remove the leading number 12 | t.is(removeLeadingNumber('0_ThisIsATest'), 'ThisIsATest'); 13 | t.is(removeLeadingNumber('000_ThisIsATest'), 'ThisIsATest'); 14 | t.is(removeLeadingNumber('12345_FAQ'), 'FAQ'); 15 | t.is(removeLeadingNumber('09870_FAQ'), 'FAQ'); 16 | t.is(removeLeadingNumber('-1_FAQ'), '-1_FAQ'); 17 | t.is(removeLeadingNumber('_FAQ'), '_FAQ'); 18 | t.is(removeLeadingNumber('ThisIsATest'), 'ThisIsATest'); 19 | }); 20 | 21 | test('test humanizesSlug function', (t) => { 22 | // it should humanize slug 23 | // t.is(humanizesSlug('0_ThisIsATest'), 'This Is A Test'); 24 | t.is(humanizesSlug('12345_FAQ'), 'FAQ'); 25 | t.is(humanizesSlug('-1_FAQ'), '-1 FAQ'); 26 | t.is(humanizesSlug('This_is_a_test'), 'This is a test'); 27 | t.is(humanizesSlug('1_FAQ'), 'FAQ'); 28 | t.is(humanizesSlug('_FAQ'), 'FAQ'); 29 | t.is(humanizesSlug('FAQ'), 'FAQ'); 30 | // t.is(humanizesSlug('0_ThisIsATest'), 'This Is A Test'); 31 | }); 32 | 33 | test('test strToSlug function', (t) => { 34 | // it should transform a string to slug slug 35 | // t.is(strToSlug('This is a test'), 'Thissatest'); 36 | t.is(strToSlug('A Test'), 'ATest'); 37 | t.is(strToSlug('A Test'), 'ATest'); 38 | // t.is(strToSlug('A_super Test'), 'AsuperTest'); 39 | }); 40 | 41 | test('test getBasename function', (t) => { 42 | // it should get the getBasename 43 | t.is(getBasename('document.txt'), 'document'); 44 | t.is(getBasename('document.test.md'), 'document.test'); 45 | }); 46 | 47 | test('test getExtension function', (t) => { 48 | // it should get the extension 49 | t.is(getExtension('document.txt'), 'txt'); 50 | t.is(getExtension('document.test.txt'), 'txt'); 51 | t.is(getExtension('.txt'), ''); 52 | t.is(getExtension('qwe'), ''); 53 | t.is(getExtension(''), ''); 54 | }); 55 | --------------------------------------------------------------------------------