├── .eslintrc ├── .gitignore ├── .npmignore ├── README.md ├── bin ├── chapter.js ├── cmd.js ├── count.js └── server.js ├── chapters.yml ├── commitlint.config.js ├── dist ├── assets │ ├── css │ │ ├── app.css │ │ ├── bulma.min.css │ │ ├── collapse.svg │ │ ├── expand.svg │ │ ├── next-disabled.svg │ │ ├── next.svg │ │ ├── previous-disabled.svg │ │ ├── previous.svg │ │ ├── size.svg │ │ ├── toc.svg │ │ └── up.svg │ └── js │ │ └── app.js ├── cli-options.html ├── favicon.ico ├── index.html ├── sub-commands.html └── template-variables.html ├── docs ├── cli-options.md ├── sub-commands.md └── template-variables.md ├── lib ├── chapters.js ├── config.js ├── copy.js ├── index.js ├── log4js.config.json ├── page.js ├── theme.js └── utils.js ├── loppo.yml ├── loppo.yml.default ├── package.json ├── test ├── chapters.test.js ├── config.test.js ├── copy.test.js ├── fixture │ ├── chapters │ │ ├── chapters-file-already-exists │ │ │ └── chapters.yml │ │ ├── complex-docs-directory-structure │ │ │ └── docs │ │ │ │ ├── a.md │ │ │ │ └── b │ │ │ │ └── c │ │ │ │ └── d.md │ │ ├── empty-chapters-file │ │ │ └── chapters.yml │ │ ├── exclude-complex-empty-directory │ │ │ └── docs │ │ │ │ ├── a.md │ │ │ │ └── images │ │ │ │ └── dir3 │ │ │ │ └── b.md │ │ ├── exclude-double-empty-directory │ │ │ └── docs │ │ │ │ └── a.md │ │ ├── exclude-empty-directory │ │ │ └── docs │ │ │ │ └── a.md │ │ ├── generating-chapters-file-complex-format │ │ │ └── docs │ │ │ │ ├── a.md │ │ │ │ └── b.md │ │ ├── generating-chapters-file-filter │ │ │ └── docs │ │ │ │ ├── _a.txt │ │ │ │ └── a.txt │ │ ├── no-docs-directory-exists │ │ │ └── readme.md │ │ └── simple-generating-chapters-file │ │ │ └── docs │ │ │ ├── a.md │ │ │ └── b.md │ ├── config │ │ ├── no-options-no-default │ │ │ └── .gitkeep │ │ ├── no-options-with-config │ │ │ └── loppo.yml │ │ ├── options-no-config │ │ │ └── .gitkeep │ │ └── options-with-config │ │ │ └── loppo.yml │ ├── copy │ │ ├── copy-to-output-directory │ │ │ └── themes │ │ │ │ └── oceandeep │ │ │ │ └── index.html │ │ ├── customization-false │ │ │ └── .gitkeep │ │ ├── customization-true │ │ │ └── loppo-theme │ │ │ │ └── assets │ │ │ │ └── js │ │ │ │ └── app.js │ │ └── excludes-template-files │ │ │ └── themes │ │ │ └── oceandeep │ │ │ └── index.template │ ├── makeContent │ │ ├── non-root-index-with-md-files │ │ │ └── docs │ │ │ │ └── dir1 │ │ │ │ ├── a.md │ │ │ │ └── b.md │ │ ├── regular-markdown-file │ │ │ └── docs │ │ │ │ └── first.md │ │ ├── root-index-with-readme │ │ │ └── README.md │ │ └── root-index-without-readme │ │ │ └── .gitkeep │ ├── theme │ │ ├── customization-true-no-themedir-exists │ │ │ └── .gitkeep │ │ ├── customization-true-old-themedir-exists │ │ │ └── themes │ │ │ │ └── oceandeep │ │ │ │ └── page.template │ │ ├── customization-true-themedir-exists │ │ │ └── loppo-theme │ │ │ │ └── page.template │ │ ├── includes │ │ │ └── themes │ │ │ │ └── oceandeep │ │ │ │ ├── author.template │ │ │ │ ├── date.template │ │ │ │ ├── page.template │ │ │ │ └── title.template │ │ ├── theme-exists │ │ │ └── themes │ │ │ │ └── oceandeep │ │ │ │ └── page.template │ │ └── theme-not-exists │ │ │ └── themes │ │ │ └── .gitkeep │ └── writePage │ │ └── simple-write │ │ └── .gitkeep ├── makeBreadcrumb.test.js ├── makeBuildTime.test.js ├── makeContent.test.js ├── makeIndex.test.js ├── makeNextPageObject.test.js ├── makePageTitle.test.js ├── makePreviousPageObject.test.js ├── makeRelativeRootPath.test.js ├── markdownRender.test.js ├── theme.test.js ├── utils │ ├── isHomepage.test.js │ ├── makeChapterList.test.js │ ├── makeChaptersOrigin.test.js │ └── siteId.test.js └── writePage.test.js └── utils ├── escapeTitle.js ├── findRootPosition.js ├── isHomepage.js ├── makeBreadcrumb.js ├── makeBreadcrumbOrigin.js ├── makeChapterList.js ├── makeChaptersOrigin.js ├── makeNextPageObject.js ├── makeNextPageOrigin.js ├── makePreviousPageObject.js ├── makePreviousPageOrigin.js ├── readmeCheck.js ├── siteId.js └── themePath.js /.eslintrc: -------------------------------------------------------------------------------- 1 | { 2 | "env": { 3 | "node": true 4 | }, 5 | "globals": { 6 | }, 7 | "plugins": [ 8 | ], 9 | "extends": "airbnb", 10 | "rules": { 11 | "camelcase": 0, 12 | "comma-dangle": 0, 13 | "func-names": 0, 14 | "global-require": 0, 15 | "prefer-arrow-callback": 0, 16 | "prefer-template": 0, 17 | "no-console": 0, 18 | "no-constant-condition": 0, 19 | "no-param-reassign": 0, 20 | "no-shadow": 0, 21 | "no-unused-expressions": 0, 22 | "id-length": 0, 23 | "prefer-rest-params": 0, 24 | "react/prop-types": 0, 25 | "strict": 0, 26 | "vars-on-top": 0, 27 | "max-len": 0, 28 | "import/newline-after-import": 0, 29 | "import/no-dynamic-require": 0, 30 | "no-path-concat": 0, 31 | "prefer-spread": 0, 32 | "prefer-destructuring": 0 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | build/ 3 | npm-debug.log 4 | package-lock.json 5 | -------------------------------------------------------------------------------- /.npmignore: -------------------------------------------------------------------------------- 1 | test/ 2 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | Loppo is an extremely easy static site generator of markdown documents. You get your site with only one command. Please visit [demo](http://redux.ruanyifeng.com/). 2 | 3 | ## Features 4 | 5 | - easy config ([example](https://raw.githubusercontent.com/ruanyf/loppo/master/loppo.yml.default)) 6 | - simple site structure ([example](https://raw.githubusercontent.com/ruanyf/redux-docs/master/chapters.yml)) 7 | - friendly template syntax([example](https://raw.githubusercontent.com/ruanyf/redux-docs/master/themes/oceandeep/page.template)) 8 | - built-in [utility commands](docs/sub-commands.md) 9 | 10 | ## How to use 11 | 12 | **Attention: Loppo is still in its very early stages. Use it in production at your own risk.** 13 | 14 | First of all, arrange your documents into the following structure. 15 | 16 | ``` 17 | |- myProject 18 | |- README.md 19 | |- docs 20 | |- page1.md 21 | |- page2.md 22 | |- ... 23 | ``` 24 | 25 | Now, install Loppo. 26 | 27 | ```bash 28 | $ npm install loppo -g 29 | ``` 30 | 31 | Enter your project directory. 32 | 33 | ```bash 34 | $ cd myProject 35 | ``` 36 | 37 | Run the command. 38 | 39 | ```bash 40 | $ loppo 41 | ``` 42 | 43 | Now, Loppo will build the document site under `dist` sub-directory. After the building process, you could open the site in your browser. 44 | 45 | ```bash 46 | $ open dist/index.html 47 | ``` 48 | 49 | ## License 50 | 51 | GPL v3 52 | 53 | -------------------------------------------------------------------------------- /bin/chapter.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const promptly = require('promptly'); 4 | const fs = require('fs-extra'); 5 | const path = require('path'); 6 | const { log } = require('../lib/utils'); 7 | const readmeCheck = require('../utils/readmeCheck'); 8 | const chaptersLib = require('../lib/chapters'); 9 | 10 | function chapterHandler(option) { 11 | const chaptersFile = path.resolve(process.cwd(), 'chapters.yml'); 12 | if (fs.existsSync(chaptersFile)) { 13 | fs.removeSync(chaptersFile); 14 | } 15 | chaptersLib(option); 16 | } 17 | 18 | module.exports = { 19 | command: 'chapter', 20 | desc: 'create new chapters.yml file', 21 | builder: yargs => yargs 22 | .option('force', { 23 | alias: 'f', 24 | describe: 'chapters.yml will be deleted without confirmation', 25 | type: 'boolean', 26 | default: 'false', 27 | }), 28 | handler: (argv) => { 29 | const str = 'This command will delete your chapters.yml if existed.\nAre you sure to continue? (Y/N)'; 30 | if (!argv.force) { 31 | promptly.prompt(str, function (err, value) { 32 | const input = value.trim().toLowerCase(); 33 | if (input !== 'y' && input !== 'yes') return; 34 | chapterHandler(argv); 35 | }); 36 | } else { 37 | chapterHandler(argv); 38 | } 39 | } 40 | }; 41 | -------------------------------------------------------------------------------- /bin/cmd.js: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env node 2 | 3 | const path = require('path'); 4 | 5 | const { argv } = require('yargs') 6 | .usage('Usage: loppo [Options], loppo [Commands] [Options]') 7 | .option('dir', { 8 | alias: 'd', 9 | default: 'docs', 10 | describe: 'document directory', 11 | type: 'string' 12 | }) 13 | .option('output', { 14 | alias: 'o', 15 | default: 'dist', 16 | describe: 'output directory', 17 | type: 'string' 18 | }) 19 | .option('site', { 20 | alias: 's', 21 | default: 'Documents', 22 | describe: 'site name', 23 | type: 'string' 24 | }) 25 | .option('id', { 26 | default: require('../utils/siteId')(), 27 | describe: 'site ID', 28 | type: 'string' 29 | }) 30 | .option('direction', { 31 | default: 'ltr', 32 | describe: 'document character direction', 33 | type: 'string' 34 | }) 35 | .option('theme', { 36 | alias: 't', 37 | describe: 'theme name', 38 | type: 'string' 39 | }) 40 | .option('debug', { 41 | default: false, 42 | describe: 'debug mode', 43 | type: 'boolean' 44 | }) 45 | .option('help', { 46 | alias: 'h', 47 | describe: 'help information', 48 | type: 'boolean' 49 | }) 50 | .command(require('./server')) 51 | .command(require('./count')) 52 | .command(require('./chapter')) 53 | .help('help') 54 | .version() 55 | .example('loppo --dir docs --output dist') 56 | .example('loppo server'); 57 | 58 | if (argv.debug) { 59 | process.env.DEBUG = '*'; 60 | } 61 | 62 | if ( 63 | argv._.indexOf('server') === -1 && 64 | argv._.indexOf('count') === -1 && 65 | argv._.indexOf('chapter') === -1 66 | ) { 67 | require('../lib')(argv); 68 | } 69 | 70 | -------------------------------------------------------------------------------- /bin/count.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const path = require('path'); 4 | const fs = require('fs-extra'); 5 | const yaml = require('js-yaml'); 6 | const md = require('turpan'); 7 | const htmlToText = require('html-to-text'); 8 | const wordCount = require('wordcount'); 9 | 10 | function print(filesArr, option) { 11 | if (option.file) { 12 | const fileList = []; 13 | filesArr.forEach(f => fileList.push(f.file)); 14 | const position = fileList.indexOf(option.file); 15 | if (position === -1) { 16 | console.log('Error: cannot find ' + option.file); 17 | return; 18 | } 19 | console.log('[File] ' + option.file); 20 | console.log('[Lines] ' + filesArr[position].line); 21 | console.log('[Words] ' + filesArr[position].word); 22 | console.log('[Chars] ' + filesArr[position].char); 23 | return; 24 | } 25 | 26 | let totalLine = 0; 27 | let totalWord = 0; 28 | let totalChar = 0; 29 | filesArr.forEach((f) => { 30 | totalLine += f.line; 31 | totalWord += f.word; 32 | totalChar += f.char; 33 | 34 | if (option.detail) { 35 | console.log('[File] ' + f.file); 36 | console.log('[Lines] ' + f.line); 37 | console.log('[Words] ' + f.word); 38 | console.log('[Chars] ' + f.char); 39 | console.log(); 40 | } 41 | }); 42 | 43 | if (option.detail) { 44 | console.log('## Total'); 45 | } 46 | 47 | console.log('[Files] ' + filesArr.length); 48 | console.log('[Lines] ' + totalLine); 49 | console.log('[Words] ' + totalWord); 50 | console.log('[Chars] ' + totalChar); 51 | } 52 | 53 | module.exports = { 54 | command: 'count', 55 | desc: 'word counts for each markdown file', 56 | builder: yargs => yargs 57 | .option('file', { 58 | alias: 'f', 59 | describe: 'the file name to count', 60 | type: 'string' 61 | }) 62 | .option('detail', { 63 | alias: 'd', 64 | describe: 'detail mode', 65 | default: 'false', 66 | type: 'boolean' 67 | }), 68 | handler: (argv) => { 69 | const chaptersPath = path.resolve(process.cwd(), 'chapters.yml'); 70 | const chaptersContent = fs.readFileSync(chaptersPath, 'utf8'); 71 | const chaptersArr = yaml.safeLoad(chaptersContent); 72 | 73 | const cfgPath = path.resolve(process.cwd(), 'loppo.yml'); 74 | const cfgContent = fs.readFileSync(cfgPath, 'utf8'); 75 | const cfgObj = yaml.safeLoad(cfgContent); 76 | const docDir = cfgObj.dir; 77 | 78 | const filesArr = []; 79 | chaptersArr 80 | .filter(c => Object.keys(c)[0].substr(-3) === '.md') 81 | .forEach((c) => { 82 | const fileName = Object.keys(c)[0]; 83 | const filePath = path.resolve(process.cwd(), docDir, fileName); 84 | let fileContent = fs.readFileSync(filePath, 'utf8').trim(); 85 | const fileContentArr = fileContent.split('\n'); 86 | if (/^\s*#\s*([^#].*?)\s*$/.test(fileContentArr[0])) fileContentArr.shift(); 87 | fileContent = fileContentArr.join('\n'); 88 | 89 | const HTMLContent = md.render(fileContent); 90 | const TEXTContent = htmlToText.fromString(HTMLContent, { 91 | wordwrap: false, 92 | ignoreImage: true, 93 | ignoreHref: true 94 | }); 95 | const result = {}; 96 | result.file = fileName; 97 | result.line = TEXTContent.split('\n').length; 98 | result.word = wordCount(TEXTContent); 99 | result.char = TEXTContent.length; 100 | filesArr.push(result); 101 | }); 102 | 103 | print(filesArr, argv); 104 | } 105 | }; 106 | 107 | -------------------------------------------------------------------------------- /bin/server.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const connect = require('connect'); 4 | const serveStatic = require('serve-static'); 5 | const path = require('path'); 6 | 7 | module.exports = { 8 | command: 'server', 9 | desc: 'build the docs and run a web server', 10 | handler: (argv) => { 11 | const option = require('../lib')(argv); 12 | connect() 13 | .use(serveStatic(path.resolve(process.cwd(), option.output))) 14 | .listen(8080, function () { 15 | console.log('Server running on 8080...'); 16 | }); 17 | } 18 | }; 19 | -------------------------------------------------------------------------------- /chapters.yml: -------------------------------------------------------------------------------- 1 | - cli-options.md: Command line options 2 | - sub-commands.md: Sub-commands 3 | - template-variables.md: Template variables 4 | -------------------------------------------------------------------------------- /commitlint.config.js: -------------------------------------------------------------------------------- 1 | module.exports = {extends: ['@commitlint/config-conventional']} 2 | -------------------------------------------------------------------------------- /dist/assets/css/app.css: -------------------------------------------------------------------------------- 1 | /* 2 | * general 3 | */ 4 | 5 | :root { 6 | font-size: 16px; 7 | font-family: "FZLanTingHei", "Helvetica Neue", "PingFang SC", "Hiragino Sans GB", "Microsoft YaHei", "Hiragino Kaku Gothic Pro", Meiryo, "Malgun Gothic", sans-serif; 8 | box-sizing: border-box; 9 | } 10 | 11 | body { 12 | font-size: 1.25rem; 13 | } 14 | 15 | code { 16 | font-size: 1.25rem; 17 | margin-left: 0.25rem; 18 | margin-right: 0.25rem; 19 | } 20 | 21 | .link-disabled { 22 | pointer-events: none; 23 | } 24 | 25 | .progress-indicator { 26 | position: fixed; 27 | top: 0; 28 | left: 0; 29 | height: 3px; 30 | background-color: #ffdd57; 31 | } 32 | 33 | 34 | /* 35 | * navbar 36 | */ 37 | 38 | .navbar { 39 | background-color: #3273dc; 40 | color: #FFF; 41 | position: fixed; 42 | top: 0; 43 | right: 0; 44 | width: 100%; 45 | height: 3.3125rem; 46 | padding-bottom: 0; 47 | margin-right: 0; 48 | margin-top: 0; 49 | margin-bottom:0; 50 | } 51 | 52 | .logo { 53 | margin-top: 0.3rem; 54 | margin-left: 1rem; 55 | } 56 | 57 | .logo a { 58 | color: #FFF; 59 | } 60 | 61 | .logo a:hover { 62 | color: rgba(255, 255, 255, 0.8); 63 | } 64 | 65 | .site-title { 66 | text-align: center; 67 | } 68 | 69 | 70 | /* 71 | * book-toc 72 | */ 73 | 74 | .book-toc { 75 | position: fixed; 76 | top: 3.3125rem; 77 | right: 0; 78 | opacity: 0.9; 79 | transition: all 1s; 80 | color: rgba(0, 0, 0, 0.4); 81 | line-height: 2; 82 | height: 75vh; 83 | width: 20rem; 84 | overflow: scroll; 85 | z-index: 9; 86 | } 87 | 88 | .book-toc h3 { 89 | margin-bottom: 1.5rem; 90 | } 91 | 92 | .book-toc a { 93 | color: rgba(0, 0, 0, 0.7); 94 | } 95 | 96 | .book-toc a:hover { 97 | color: rgba(0, 0, 0, 0.9); 98 | text-decoration: underline; 99 | } 100 | 101 | .book-toc .chapter-item-current a { 102 | color: #ff3860; 103 | font-weight: 700; 104 | } 105 | 106 | .book-toc ul { 107 | margin-top: 0; 108 | margin-left: 0.5rem; 109 | margin-right: 0.5rem; 110 | } 111 | 112 | .book-toc ul.chapter-area { 113 | counter-reset: chapter; 114 | } 115 | 116 | .book-toc ul.chapter-area > li { 117 | list-style-type: none; 118 | } 119 | 120 | .book-toc ul.chapter-area > li::before { 121 | counter-increment: chapter; 122 | content: counter(chapter) ". "; 123 | } 124 | 125 | .book-toc ul.chapter-level-1 { 126 | counter-reset: article; 127 | } 128 | 129 | .book-toc ul.chapter-level-1 > li { 130 | list-style-type: none; 131 | } 132 | 133 | .book-toc ul.chapter-level-1 > li::before { 134 | counter-increment: article; 135 | content: counter(chapter) "." counter(article) " "; 136 | } 137 | 138 | .book-toc .icon-expand { 139 | width: 18px; 140 | height: 18px; 141 | content: url(expand.svg); 142 | } 143 | 144 | .book-toc .icon-collapse { 145 | width: 18px; 146 | height: 18px; 147 | content: url(collapse.svg); 148 | } 149 | 150 | 151 | /* 152 | * content 153 | */ 154 | 155 | .content { 156 | background-color: #FFF; 157 | color: #333; 158 | margin-top: 3.3125rem; 159 | padding-left: 1rem; 160 | padding-right: 1rem; 161 | line-height: 1.6; 162 | } 163 | 164 | .breadcrumb-area { 165 | padding-top: 1rem; 166 | } 167 | 168 | h1.article-title { 169 | margin-bottom: 3rem; 170 | } 171 | 172 | .sticky { 173 | bottom: 0px; 174 | position: fixed; 175 | margin-left: 0; 176 | margin-right: 0; 177 | padding-left: 0; 178 | padding-right: 0; 179 | padding-bottom: 0px; 180 | margin-bottom: 0px; 181 | } 182 | 183 | .article-bar { 184 | margin-top: 3rem; 185 | padding-top: 1rem; 186 | margin-left: -10px; 187 | margin-right: -10px; 188 | border-top: 1px solid #bdc3c7; 189 | position: -webkit-sticky; 190 | position: sticky; 191 | bottom: 0; 192 | background: white; 193 | } 194 | 195 | .article-bar .icon-next { 196 | content: url(next.svg); 197 | } 198 | 199 | .article-bar .icon-previous { 200 | content: url(previous.svg); 201 | } 202 | 203 | .article-bar .icon-up { 204 | content: url(up.svg); 205 | } 206 | 207 | .article-bar .icon-size { 208 | content: url(size.svg); 209 | } 210 | 211 | .article-bar .icon-toc { 212 | content: url(toc.svg); 213 | } 214 | 215 | .article-bar .icon-previous-disabled { 216 | content: url(previous-disabled.svg); 217 | } 218 | 219 | .article-bar .icon-next-disabled { 220 | content: url(next-disabled.svg); 221 | } 222 | 223 | .article-bar .level-item .link-content { 224 | display: none; 225 | } 226 | 227 | 228 | /* 229 | * article 230 | */ 231 | 232 | .article { 233 | counter-reset: firstLevelTitle; 234 | } 235 | 236 | .article > h2::before { 237 | counter-increment: firstLevelTitle; 238 | content: counter(firstLevelTitle) '. '; 239 | } 240 | 241 | .article > h2:only-of-type::before { 242 | content: ''; 243 | } 244 | 245 | .article > h2 { 246 | counter-reset: secondLevelTitle; 247 | } 248 | 249 | .article > h3::before { 250 | counter-increment: secondLevelTitle; 251 | content: counter(firstLevelTitle) '.' counter(secondLevelTitle) ' '; 252 | } 253 | 254 | .article .first-level-collapse { 255 | display: none; 256 | } 257 | 258 | /* 259 | * article-toc 260 | */ 261 | 262 | .article-toc { 263 | border: 1px solid #aaa; 264 | background-color: #f9f9f9; 265 | display: table; 266 | padding: 1rem; 267 | } 268 | 269 | .article-toc ul { 270 | margin-left: 0.5rem; 271 | margin-right: 0.5rem; 272 | } 273 | 274 | .article-toc > ul { 275 | counter-reset: articleTOCFirstLevel; 276 | } 277 | 278 | .article-toc > ul > li { 279 | list-style-type: none; 280 | counter-reset: articleTOCSecondLevel; 281 | } 282 | 283 | .article-toc > ul > li::before { 284 | counter-increment: articleTOCFirstLevel; 285 | content: counter(articleTOCFirstLevel) '. '; 286 | } 287 | 288 | .article-toc > ul > li > ul > li { 289 | list-style-type: none; 290 | } 291 | 292 | .article-toc > ul > li > ul > li::before { 293 | counter-increment: articleTOCSecondLevel; 294 | content: counter(articleTOCFirstLevel) '.' counter(articleTOCSecondLevel) ' '; 295 | } 296 | 297 | 298 | /* 299 | * foot 300 | */ 301 | 302 | .foot { 303 | background-color: #FFF; 304 | border-top: 1px solid #bdc3c7; 305 | color: #b86bff; 306 | font-size: 0.9rem; 307 | } 308 | 309 | .columns.foot { 310 | margin-bottom: 0; 311 | } 312 | 313 | .build-by { 314 | margin-left: 1rem; 315 | } 316 | 317 | 318 | /* 319 | * highlight 320 | */ 321 | 322 | .hljs { 323 | margin-bottom: 1rem; 324 | border-radius: 0.5rem; 325 | overflow: auto; 326 | } 327 | 328 | .hljs code { 329 | background: #23241f; 330 | color: #f8f8f2; 331 | direction: ltr; 332 | } 333 | 334 | /* 335 | 336 | Monokai Sublime style. Derived from Monokai by noformnocontent http://nn.mit-license.org/ 337 | 338 | */ 339 | 340 | .hljs { 341 | display: block; 342 | overflow-x: auto; 343 | padding: 0.5em; 344 | background: #23241f; 345 | } 346 | 347 | .hljs, 348 | .hljs-tag, 349 | .hljs-subst { 350 | color: #f8f8f2; 351 | } 352 | 353 | .hljs-strong, 354 | .hljs-emphasis { 355 | color: #a8a8a2; 356 | } 357 | 358 | .hljs-bullet, 359 | .hljs-quote, 360 | .hljs-number, 361 | .hljs-regexp, 362 | .hljs-literal, 363 | .hljs-link { 364 | color: #ae81ff; 365 | } 366 | 367 | .hljs-code, 368 | .hljs-title, 369 | .hljs-section, 370 | .hljs-selector-class { 371 | color: #a6e22e; 372 | } 373 | 374 | .hljs-strong { 375 | font-weight: bold; 376 | } 377 | 378 | .hljs-emphasis { 379 | font-style: italic; 380 | } 381 | 382 | .hljs-keyword, 383 | .hljs-selector-tag, 384 | .hljs-name, 385 | .hljs-attr { 386 | color: #f92672; 387 | } 388 | 389 | .hljs-symbol, 390 | .hljs-attribute { 391 | color: #66d9ef; 392 | } 393 | 394 | .hljs-params, 395 | .hljs-class .hljs-title { 396 | color: #f8f8f2; 397 | } 398 | 399 | .hljs-string, 400 | .hljs-type, 401 | .hljs-built_in, 402 | .hljs-builtin-name, 403 | .hljs-selector-id, 404 | .hljs-selector-attr, 405 | .hljs-selector-pseudo, 406 | .hljs-addition, 407 | .hljs-variable, 408 | .hljs-template-variable { 409 | color: #e6db74; 410 | } 411 | 412 | .hljs-comment, 413 | .hljs-deletion, 414 | .hljs-meta { 415 | color: #75715e; 416 | } 417 | 418 | -------------------------------------------------------------------------------- /dist/assets/css/collapse.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | -------------------------------------------------------------------------------- /dist/assets/css/expand.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | -------------------------------------------------------------------------------- /dist/assets/css/next-disabled.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | -------------------------------------------------------------------------------- /dist/assets/css/next.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | -------------------------------------------------------------------------------- /dist/assets/css/previous-disabled.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | -------------------------------------------------------------------------------- /dist/assets/css/previous.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | -------------------------------------------------------------------------------- /dist/assets/css/size.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | -------------------------------------------------------------------------------- /dist/assets/css/toc.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | 49 | 50 | 51 | 52 | 53 | 54 | 55 | 56 | 57 | 58 | 59 | 60 | 61 | 62 | 63 | 64 | 65 | -------------------------------------------------------------------------------- /dist/assets/css/up.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | -------------------------------------------------------------------------------- /dist/assets/js/app.js: -------------------------------------------------------------------------------- 1 | /* set content's min-height */ 2 | (function () { 3 | var content = document.querySelector('.content'); 4 | var navbarHeight = 5 | window.getComputedStyle(document.querySelector('.navbar')) 6 | .getPropertyValue('height'); 7 | var footHeight = 8 | window.getComputedStyle(document.querySelector('.foot')) 9 | .getPropertyValue('height'); 10 | var minHeight = document.documentElement.clientHeight 11 | - parseInt(navbarHeight) 12 | - parseInt(footHeight); 13 | content.style.minHeight = minHeight + 'px'; 14 | })(); 15 | 16 | 17 | /* insert article's toc */ 18 | (function () { 19 | var article = document.querySelector('.article'); 20 | var firstH2Title = article.querySelector('h2'); 21 | var h2TitleNumber = article.querySelectorAll('h2'); 22 | 23 | if (firstH2Title && LOPPO.article_toc && h2TitleNumber.length >= 2) { 24 | var toc_div = document.createElement('div'); 25 | toc_div.setAttribute('class', 'article-toc'); 26 | toc_div.innerHTML = '

Contents

' + LOPPO.article_toc; 27 | article.insertBefore(toc_div, firstH2Title); 28 | } 29 | })(); 30 | 31 | 32 | /* hashchange handler */ 33 | window.onhashchange = function () { 34 | var hash = location.hash; 35 | if (!hash) return; 36 | var nav = document.querySelector('.navbar'); 37 | var navHeight = nav.getBoundingClientRect().height; 38 | window.scrollBy(0, -1 * navHeight); 39 | }; 40 | 41 | 42 | /* content font size */ 43 | (function () { 44 | var content = document.querySelector('.content'); 45 | var sizeArr = ['is-medium', null, 'is-large']; 46 | var currentSize = 1; 47 | var switcher = document.querySelector('.article-bar .level-item .link-item-size'); 48 | switcher.onclick = function (e) { 49 | content.classList.remove(sizeArr[currentSize]); 50 | currentSize += 1; 51 | if (currentSize > 2) currentSize -= 3; 52 | content.classList.add(sizeArr[currentSize]); 53 | }; 54 | })(); 55 | 56 | 57 | /* toggle book toc */ 58 | (function () { 59 | var toc = document.querySelector('.book-toc'); 60 | var switcher = document.querySelector('.article-bar .level-item .link-item-toc'); 61 | switcher.onclick = function (e) { 62 | toc.classList.toggle('is-hidden'); 63 | }; 64 | var closeButton = toc.querySelector('.title-close'); 65 | closeButton.onclick = function (e) { 66 | toc.classList.toggle('is-hidden'); 67 | }; 68 | })(); 69 | 70 | 71 | /* toggle first level directory */ 72 | (function () { 73 | var firstLevelDir = document.querySelectorAll('.book-toc .chapter-level-1:not(.chapter-level-1-current)'); 74 | firstLevelDirArr = Array.prototype.slice.call(firstLevelDir); 75 | firstLevelDirArr.forEach(function (i) { 76 | i.classList.add('is-hidden'); 77 | }); 78 | 79 | var firstLevelCollapse = document.querySelectorAll('.book-toc .first-level-collapse'); 80 | firstLevelCollapseArr = Array.prototype.slice.call(firstLevelCollapse); 81 | firstLevelCollapseArr.forEach(function (i) { 82 | i.onclick = function (e) { 83 | e.currentTarget.parentElement.nextSibling.classList.toggle('is-hidden'); 84 | var icon = i.querySelector('.icon'); 85 | icon.classList.toggle('icon-expand'); 86 | icon.classList.toggle('icon-collapse'); 87 | insert_icon_image('expand'); 88 | insert_icon_image('collapse'); 89 | }; 90 | }); 91 | })(); 92 | 93 | 94 | /* insert icon image */ 95 | function insert_icon_image(iconName) { 96 | if (iconName === undefined) { 97 | var icons = document.querySelectorAll('.icon'); 98 | } else { 99 | var icons = document.querySelectorAll('.icon-' + iconName); 100 | } 101 | var iconsArr = Array.prototype.slice.call(icons); 102 | iconsArr.forEach(function (i) { 103 | var imgOld = i.querySelector('img'); 104 | if (imgOld) i.removeChild(imgOld); 105 | var img = document.createElement('img'); 106 | if (iconName === undefined) { 107 | img.src = LOPPO.relative_root_path + 'assets/css/' + i.dataset.icon + '.svg'; 108 | } else { 109 | img.src = LOPPO.relative_root_path + 'assets/css/' + iconName + '.svg'; 110 | } 111 | i.appendChild(img); 112 | }); 113 | } 114 | insert_icon_image(); 115 | 116 | 117 | /* sticky article bar */ 118 | (function () { 119 | var article = document.querySelector('.article-container'); 120 | var articleWidth = article.getBoundingClientRect().width; 121 | var bar = document.querySelector('.article-bar'); 122 | bar.style.width = articleWidth + 'px'; 123 | var foot = document.querySelector('.foot'); 124 | 125 | var placeholder = document.createElement('div'); 126 | var barWidth = bar.getClientRects()[0].width; 127 | var barHeight = bar.getClientRects()[0].height; 128 | foot.style.height = barHeight + 'px'; 129 | placeholder.style.width = barWidth + 'px'; 130 | placeholder.style.height = barHeight + 'px'; 131 | var isAdded = false; 132 | 133 | function throttle(f) { 134 | var mark = Date.now(); 135 | return function () { 136 | var now = Date.now(); 137 | if ((now - mark) < 300) return; 138 | mark = now; 139 | f(); 140 | }; 141 | } 142 | 143 | function toggleSticky() { 144 | setTimeout(function () { 145 | var footTop = foot.getBoundingClientRect().top; 146 | if (!isAdded && footTop > (document.documentElement.clientHeight + barHeight + 10)) { 147 | isAdded = true; 148 | article.insertBefore(placeholder, bar); 149 | bar.classList.add('sticky'); 150 | } else if (isAdded && footTop <= (document.documentElement.clientHeight + barHeight + 10)) { 151 | article.removeChild(placeholder); 152 | bar.classList.remove('sticky'); 153 | isAdded = false; 154 | } 155 | }, 0); 156 | } 157 | 158 | toggleSticky(); 159 | window.addEventListener('scroll', throttle(toggleSticky)); 160 | })(); 161 | 162 | 163 | /* progress indicator */ 164 | (function () { 165 | // var pageHeight = document.documentElement.scrollHeight; 166 | var article = document.querySelector('.article'); 167 | var viewportHeight = document.documentElement.clientHeight; 168 | var articleHeight = article.getBoundingClientRect().height; 169 | var prog = document.querySelector('.progress-indicator'); 170 | window.addEventListener('scroll', function () { 171 | window.requestAnimationFrame(function () { 172 | var perc = Math.max(0, Math.min(1, (viewportHeight - article.getBoundingClientRect().top) / articleHeight)); 173 | updateProgress(perc); 174 | }); 175 | }); 176 | function updateProgress(perc) { 177 | prog.style.width = perc * 100 + '%'; 178 | } 179 | })(); 180 | 181 | /* support mermaid.js */ 182 | (function () { 183 | function onNextFrame(callback) { 184 | return () => { 185 | setTimeout(function () { 186 | window.requestAnimationFrame(callback) 187 | }, 0) 188 | }; 189 | } 190 | 191 | function addScript(src, callback) { 192 | const head = document.getElementsByTagName('head')[0]; 193 | const script = document.createElement('script'); 194 | script.setAttribute('type', 'text/javascript'); 195 | script.setAttribute('charset', 'utf-8'); 196 | script.setAttribute('src', src); 197 | script.onload = callback; 198 | head.appendChild(script); 199 | } 200 | 201 | function addStyle(src) { 202 | const head = document.getElementsByTagName('head')[0]; 203 | const link = document.createElement('link'); 204 | link.type = 'text/css'; 205 | link.rel = 'stylesheet'; 206 | link.href = src; 207 | head.appendChild(link); 208 | } 209 | 210 | // mermaid 211 | function embedMermaid() { 212 | const mermaidDivs = document.querySelectorAll('.mermaid'); 213 | const mermaidArr = Array.prototype.slice.call(mermaidDivs); 214 | if (!mermaidArr.length) return; 215 | function mermaidHandler() { 216 | window.mermaid.initialize({ 217 | startOnLoad: false, 218 | logLevel: 4, 219 | gantt: { 220 | axisFormatter: [ 221 | // Within a day 222 | ["%I:%M", function (d) { 223 | return d.getHours(); 224 | }], 225 | // Monday a week 226 | ["%m-%d", function (d) { 227 | return d.getDay() == 1; 228 | }], 229 | // Day within a week (not monday) 230 | ["%m-%e", function (d) { 231 | return d.getDay() && d.getDate() != 1; 232 | }], 233 | // within a month 234 | ["%m-%e", function (d) { 235 | return d.getDate() != 1; 236 | }], 237 | // Month 238 | ["%m-%e", function (d) { 239 | return d.getMonth(); 240 | }] 241 | ] 242 | }, 243 | }); 244 | mermaidArr.forEach((f, i) => { 245 | if (f.dataset["processed"] === 'true') return; 246 | f.innerHTML = decodeURIComponent(f.dataset["source"]); 247 | f.style.color = 'white'; 248 | (onNextFrame(() => { 249 | window.mermaid.init(undefined, f); 250 | if (!('eventAttached' in f.dataset)) { 251 | f.dataset['eventAttached'] = true; 252 | } 253 | f.style.color = 'inherit'; 254 | }))(); 255 | }); 256 | } 257 | 258 | if (window.mermaid) { 259 | mermaidHandler(); 260 | } else { 261 | addStyle('https://cdn.rawgit.com/knsv/mermaid/7.0.0/dist/mermaid.css'); 262 | addScript( 263 | 'https://cdn.rawgit.com/knsv/mermaid/7.0.0/dist/mermaid.min.js', 264 | mermaidHandler 265 | ); 266 | } 267 | } 268 | 269 | window.addEventListener('load', embedMermaid, false); 270 | })(); 271 | -------------------------------------------------------------------------------- /dist/cli-options.html: -------------------------------------------------------------------------------- 1 | Command line options - Documents

Command line options

Loppo has some command line options.

--dir, -d #

--dir or -d sets the document directory which keeps the original Markdown files. docs is the default directory.

$ loppo --dir my_docs
5 | 

--output, -o #

--output or -o sets the output directory which keeps the generated documents. dist is the default directory.

$ loppo --output my_site
6 | 

--site, -s #

--site or -s sets the site's name. Documents is the default.

$ loppo --site "My Documents"
7 | 

--theme, -t #

--theme or -t sets a site's theme. loppo-theme-oceandeep is the default.

$ loppo --theme oceandeep
8 | 

--id #

--id sets a site's ID (default is the dir name of the project).

--direction #

--direction sets the document's character direction. ltr is the default. It also could be setted as rtl.

The option needs the support of the site theme.

--debug #

--debug opens Loppo's debug mode.

--version, -v #

--version or -v shows Loppo's version information.

--help #

--help gives Loppo's commandline usage.

示例:
loppo --dir docs --output dist

Build by Loppo 0.6.24
-------------------------------------------------------------------------------- /dist/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ruanyf/loppo/962f08b3527ebb87d23611e2c2df4bea46d95502/dist/favicon.ico -------------------------------------------------------------------------------- /dist/index.html: -------------------------------------------------------------------------------- 1 | Documents

Documents

Loppo is an extremely easy static site generator of markdown documents. You get your site with only one command. Please visit demo.

Features #

How to use #

Attention: Loppo is still in its very early stages. Use it in production at your own risk.

First of all, arrange your documents into the following structure.

|- myProject
 5 |    |- README.md
 6 |    |- docs
 7 |       |- page1.md
 8 |       |- page2.md
 9 |       |- ...
10 | 

Now, install Loppo.

$ npm install loppo -g
11 | 

Enter your project directory.

$ cd myProject
12 | 

Run the command.

$ loppo
13 | 

Now, Loppo will build the document site under dist sub-directory. After the building process, you could open the site in your browser.

$ open dist/index.html
14 | 

License #

GPL v3

Table of Contents

Build by Loppo 0.6.24
-------------------------------------------------------------------------------- /dist/sub-commands.html: -------------------------------------------------------------------------------- 1 | Sub-commands - Documents

Sub-commands

Loppo supports two git-style subcommands.

  • loppo server
  • loppo count
  • loppo chapter

loppo server #

loppo server builds the document site at first, and thereafter launch a web server on 8080 port for dist sub-directory.

$ loppo server
 5 | 

After running the comand, you could visit http://127.0.0.1:8080 in your browser.

This command is helpful for preview when you develop your documents.

loppo count #

loppo count will output statistic information of your documents.

$ loppo count
 6 | 
 7 | [Files] 56
 8 | [Lines] 8885
 9 | [Words] 51555
10 | [Chars] 362931
11 | 

Attention, before using this command, chapters.yml must already be existed. Otherwise you will get an error.

loppo count has two options.

# output every markdown file's statistic information
12 | $ loppo count --detail
13 | 
14 | # output a specified markdown file's statistic information
15 | # the file path should be same as the corresponding item in chapters.yml
16 | $ loppo count -f some.md
17 | 

loppo chapter #

loppo chapter will re-create chapters.yml and add new Markdown files into it.

Attention, the old chapters.yml will be deleted after running the command.

$ loppo chapter
18 | 
19 | This command will delete your chapters.yml if existed.
20 | Are you sure to continue? (Y/N)
21 | 

If you want to skip the confirmation, use --force option.

$ loppo chapter --force
22 | 
Build by Loppo 0.6.24
-------------------------------------------------------------------------------- /dist/template-variables.html: -------------------------------------------------------------------------------- 1 | Template variables - Documents

Template variables

Loppo provides some template variables used in templates.

They could be divided into two categories: site variables and page variables.

Site variables #

The site variables are the same within the whole site.

  • site
  • id
  • dir
  • chapters
  • chaptersOrigin
  • chapterList
  • loppo_version

option.site #

option.site is the site name (default is Documents).

option.id #

option.id is the site id (default is the dir name of the project).

option.dir #

option.dir is the document directory of the repo.

option.chapters #

option.chapters is an array which includes all .md files and directories in the docs directory.

[
 5 |   {'a.md': 'Title A'},
 6 |   {'dir1/': 'dir1'},
 7 |   {'dir1/b.md': 'Title B'}
 8 | ]
 9 | 

If the doc directory has nothing, option.chapters will be an empty array.

option.chaptersOrigin #

option.chaptersOrigin is just yet another form of option.chapters with different data structure.

[
10 |   { origin: 'a.md', path: 'a.html', text: 'Title A', nextLevelBegins: false, currentLevelEnds: false },
11 |   { origin: 'dir1/', path: 'dir1/index.html', text: 'dir1', nextLevelBegins: true, currentLevelEnds: false },
12 |   { origin: 'dir1/b.md', path: 'dir1/b.html', text: 'Title B', nextLevelBegins: false, currentLevelEnds: true, currentLevelEndNum: 2}
13 | ]
14 | 

Fields

  • origin: origin path of Markdown file
  • path: path of HTML file
  • text: title of Markdown file
  • nextLevelBegins: a Boolean value indicating a diretory begins
  • currentLevelEnds: a Boolean value indicating a directory ends
  • currentLevelEndNum: a Number value indicating how many directory levels ends here, only exists when currentLevelEnds is true

option.chapterList #

option.chapterList is a HTML string converted from option.chapters.

option.loppo_version #

option.loppo_version is the version number of Loppo.

Page variables #

Page variables are different for every document page.

  • current_path
  • isHomepage
  • page_title
  • content
  • previous_page_object
  • previousPageOrigin
  • previous_page
  • next_page_object
  • nextPageOrigin
  • next_page
  • relative_root_path
  • build_time
  • breadcrumbOrigin
  • breadcrumb
  • toc

option.current_path #

option.current_path is the relative path of current page, such like dir1/example.md.

For the index page of document directory, option.current_path is /.

option.content #

option.content is the HTML markup of current page converted from markdown. It has three posibilities.

  • For regular .md file, option.content is its markdown content.
  • For the root directory (docs directory), option.content is the content of README.md under the project root directory (not docs directory)。If no README.mdoption.content is an empty string.
  • For sub-directories, option.content is all .md files and sub-directories directly under it.

option.isHomepage #

option.isHomepage is a boolean value to indicate whether on not the current page is the homepage of the site.

option.page_title #

option.page_title is the page name of a document page.

  • For sub-directories, it is option.site.
  • For root directory, it is the <h1> title of README.md. If not, it is option.site.
  • For regular .md file, it is the <h1> title of the file. If not, it is the title in chapters.yml.

option.previous_page_object #

option.previous_page_object is an object which represents the previous page of current page.

  • For the first page, it is null.
  • For other pages, it is the previous item before the current page in chapters.yml.

For example, current page is b.md as following. Then option.previous_page is { 'b.md': 'Title B' }.

- a.md: Title A
15 | - b.md: Title B
16 | 

Attention, if current page is the first item of chapters.yml and is not index.mdoption.previous_page will be { 'index.md': 'Home' }.

option.previousPageOrigin #

option.previousPageOrigin is yet another form of option.previous_page_object with different data structure.

{ origin: 'b.md', path: 'b.html', text: 'Title B' }
17 | 

option.previous_page #

option.previous_page is a HTML string converted from option.previous_page_object.

option.next_page_object #

option.next_page_object is object which represents the next page of current page.

  • For the last page, it is null.
  • For other page, it is the next item after the current page in chapters.yml.

For example, current page is a.md as following. Then option.next_page_object is { 'a.md': 'Title A' }.

- a.md: Title A
18 | - b.md: Title B
19 | 

option.nextPageOrigin #

option.nextPageOrigin is yet another form of option.next_page_object with different data structure.

{ origin: 'b.md', path: 'b.html', text: 'Title B' }
20 | 

option.next_page #

option.next_page is a HTML string converted from option.next_page_object.

option.relative_root_path #

option.relative_root_path is the relative path to the root path for the current page.

For example, if the root path is / and the current page is dir1/a.md, option.relative_root_path is ../.

option.build_time #

option.build_time is the time of building the current page, which is a JavaScript Date instance.

option.breadcrumbOrigin #

option.breadcrumbOrigin is an array containing the location information of current page.

For example, if current page is dir1/dir2/a.md, option.breadcrumb is the following.

[
21 |   { path: 'index.html', text: 'Home' },
22 |   { path: 'dir1/index.html', text: 'dir1' },
23 |   { path: 'dir1/dir2/index.html', text: 'dir2' },
24 |   { path: 'dir1/dir2/a.html', text: 'Title A' }
25 | ]
26 | 

option.breadcrumb #

option.breadcrumb is a HTML string which containing the location information of current page.

<div class="breadcrumb-area">
27 |   <a href="index.html" class="breadcrumb-item" target="_blank">Home</a>
28 |   <span class="breadcrumb-delimitor"> &gt; </span>
29 |   <a href="dir1/" class="breadcrumb-item" target="_blank">dir1</a>
30 |   <!-- ... -->
31 | </div>
32 | 

option.toc #

option.toc is the table of content of current page.

Build by Loppo 0.6.24
-------------------------------------------------------------------------------- /docs/cli-options.md: -------------------------------------------------------------------------------- 1 | # Command line options 2 | 3 | Loppo has some command line options. 4 | 5 | ## --dir, -d 6 | 7 | `--dir` or `-d` sets the document directory which keeps the original Markdown files. `docs` is the default directory. 8 | 9 | ```bash 10 | $ loppo --dir my_docs 11 | ``` 12 | 13 | ## --output, -o 14 | 15 | `--output` or `-o` sets the output directory which keeps the generated documents. `dist` is the default directory. 16 | 17 | ```bash 18 | $ loppo --output my_site 19 | ``` 20 | 21 | ## --site, -s 22 | 23 | `--site` or `-s` sets the site's name. `Documents` is the default. 24 | 25 | ```bash 26 | $ loppo --site "My Documents" 27 | ``` 28 | 29 | ## --theme, -t 30 | 31 | `--theme` or `-t` sets a site's theme. `loppo-theme-oceandeep` is the default. 32 | 33 | ```bash 34 | $ loppo --theme oceandeep 35 | ``` 36 | 37 | ## --id 38 | 39 | `--id` sets a site's ID (default is the dir name of the project). 40 | 41 | ## --direction 42 | 43 | `--direction` sets the document's character direction. `ltr` is the default. It also could be setted as `rtl`. 44 | 45 | The option needs the support of the site theme. 46 | 47 | ## --debug 48 | 49 | `--debug` opens Loppo's debug mode. 50 | 51 | ## --version, -v 52 | 53 | `--version` or `-v` shows Loppo's version information. 54 | 55 | ## --help 56 | 57 | `--help` gives Loppo's commandline usage. 58 | 59 | 60 | 61 | 62 | 63 | 示例: 64 | loppo --dir docs --output dist 65 | -------------------------------------------------------------------------------- /docs/sub-commands.md: -------------------------------------------------------------------------------- 1 | # Sub-commands 2 | 3 | Loppo supports two git-style subcommands. 4 | 5 | - loppo server 6 | - loppo count 7 | - loppo chapter 8 | 9 | ## loppo server 10 | 11 | `loppo server` builds the document site at first, and thereafter launch a web server on 8080 port for `dist` sub-directory. 12 | 13 | ```bash 14 | $ loppo server 15 | ``` 16 | 17 | After running the comand, you could visit http://127.0.0.1:8080 in your browser. 18 | 19 | This command is helpful for preview when you develop your documents. 20 | 21 | ## loppo count 22 | 23 | `loppo count` will output statistic information of your documents. 24 | 25 | ```bash 26 | $ loppo count 27 | 28 | [Files] 56 29 | [Lines] 8885 30 | [Words] 51555 31 | [Chars] 362931 32 | ``` 33 | 34 | Attention, before using this command, `chapters.yml` must already be existed. Otherwise you will get an error. 35 | 36 | `loppo count` has two options. 37 | 38 | ```bash 39 | # output every markdown file's statistic information 40 | $ loppo count --detail 41 | 42 | # output a specified markdown file's statistic information 43 | # the file path should be same as the corresponding item in chapters.yml 44 | $ loppo count -f some.md 45 | ``` 46 | 47 | ## loppo chapter 48 | 49 | `loppo chapter` will re-create `chapters.yml` and add new Markdown files into it. 50 | 51 | Attention, the old `chapters.yml` will be deleted after running the command. 52 | 53 | ```bash 54 | $ loppo chapter 55 | 56 | This command will delete your chapters.yml if existed. 57 | Are you sure to continue? (Y/N) 58 | ``` 59 | 60 | If you want to skip the confirmation, use `--force` option. 61 | 62 | ```bash 63 | $ loppo chapter --force 64 | ``` 65 | -------------------------------------------------------------------------------- /docs/template-variables.md: -------------------------------------------------------------------------------- 1 | # Template variables 2 | 3 | Loppo provides some template variables used in templates. 4 | 5 | They could be divided into two categories: site variables and page variables. 6 | 7 | ## Site variables 8 | 9 | The site variables are the same within the whole site. 10 | 11 | - site 12 | - id 13 | - dir 14 | - chapters 15 | - chaptersOrigin 16 | - chapterList 17 | - loppo_version 18 | 19 | ### option.site 20 | 21 | `option.site` is the site name (default is `Documents`). 22 | 23 | ### option.id 24 | 25 | `option.id` is the site id (default is the dir name of the project). 26 | 27 | ### option.dir 28 | 29 | `option.dir` is the document directory of the repo. 30 | 31 | ### option.chapters 32 | 33 | `option.chapters` is an array which includes all `.md` files and directories in the `docs` directory. 34 | 35 | ```javascript 36 | [ 37 | {'a.md': 'Title A'}, 38 | {'dir1/': 'dir1'}, 39 | {'dir1/b.md': 'Title B'} 40 | ] 41 | ``` 42 | 43 | If the `doc` directory has nothing, `option.chapters` will be an empty array. 44 | 45 | ### option.chaptersOrigin 46 | 47 | `option.chaptersOrigin` is just yet another form of `option.chapters` with different data structure. 48 | 49 | ```javascript 50 | [ 51 | { origin: 'a.md', path: 'a.html', text: 'Title A', nextLevelBegins: false, currentLevelEnds: false }, 52 | { origin: 'dir1/', path: 'dir1/index.html', text: 'dir1', nextLevelBegins: true, currentLevelEnds: false }, 53 | { origin: 'dir1/b.md', path: 'dir1/b.html', text: 'Title B', nextLevelBegins: false, currentLevelEnds: true, currentLevelEndNum: 2} 54 | ] 55 | ``` 56 | 57 | Fields 58 | 59 | - origin: origin path of Markdown file 60 | - path: path of HTML file 61 | - text: title of Markdown file 62 | - nextLevelBegins: a Boolean value indicating a diretory begins 63 | - currentLevelEnds: a Boolean value indicating a directory ends 64 | - currentLevelEndNum: a Number value indicating how many directory levels ends here, only exists when `currentLevelEnds` is `true` 65 | 66 | ### option.chapterList 67 | 68 | `option.chapterList` is a HTML string converted from `option.chapters`. 69 | 70 | ### option.loppo_version 71 | 72 | `option.loppo_version` is the version number of Loppo. 73 | 74 | ## Page variables 75 | 76 | Page variables are different for every document page. 77 | 78 | - current_path 79 | - isHomepage 80 | - page_title 81 | - content 82 | - previous_page_object 83 | - previousPageOrigin 84 | - previous_page 85 | - next_page_object 86 | - nextPageOrigin 87 | - next_page 88 | - relative_root_path 89 | - build_time 90 | - breadcrumbOrigin 91 | - breadcrumb 92 | - toc 93 | 94 | ### option.current_path 95 | 96 | `option.current_path` is the relative path of current page, such like `dir1/example.md`. 97 | 98 | For the index page of document directory, `option.current_path` is `/`. 99 | 100 | ### option.content 101 | 102 | `option.content` is the HTML markup of current page converted from markdown. It has three posibilities. 103 | 104 | - For regular `.md` file, `option.content` is its markdown content. 105 | - For the root directory (`docs` directory), `option.content` is the content of `README.md` under the project root directory (not `docs` directory)。If no `README.md`,`option.content` is an empty string. 106 | - For sub-directories, `option.content` is all `.md` files and sub-directories directly under it. 107 | 108 | ### option.isHomepage 109 | 110 | `option.isHomepage` is a boolean value to indicate whether on not the current page is the homepage of the site. 111 | 112 | ### option.page_title 113 | 114 | `option.page_title` is the page name of a document page. 115 | 116 | - For sub-directories, it is `option.site`. 117 | - For root directory, it is the `

` title of `README.md`. If not, it is `option.site`. 118 | - For regular `.md` file, it is the `

` title of the file. If not, it is the title in `chapters.yml`. 119 | 120 | ### option.previous_page_object 121 | 122 | `option.previous_page_object` is an object which represents the previous page of current page. 123 | 124 | - For the first page, it is `null`. 125 | - For other pages, it is the previous item before the current page in `chapters.yml`. 126 | 127 | For example, current page is `b.md` as following. Then `option.previous_page` is `{ 'b.md': 'Title B' }`. 128 | 129 | ```javascript 130 | - a.md: Title A 131 | - b.md: Title B 132 | ``` 133 | 134 | Attention, if current page is the first item of `chapters.yml` and is not `index.md`,`option.previous_page` will be `{ 'index.md': 'Home' }`. 135 | 136 | ### option.previousPageOrigin 137 | 138 | `option.previousPageOrigin` is yet another form of `option.previous_page_object` with different data structure. 139 | 140 | ```javascript 141 | { origin: 'b.md', path: 'b.html', text: 'Title B' } 142 | ``` 143 | 144 | ### option.previous_page 145 | 146 | `option.previous_page` is a HTML string converted from `option.previous_page_object`. 147 | 148 | ### option.next_page_object 149 | 150 | `option.next_page_object` is object which represents the next page of current page. 151 | 152 | - For the last page, it is `null`. 153 | - For other page, it is the next item after the current page in `chapters.yml`. 154 | 155 | For example, current page is `a.md` as following. Then `option.next_page_object` is `{ 'a.md': 'Title A' }`. 156 | 157 | ```javascript 158 | - a.md: Title A 159 | - b.md: Title B 160 | ``` 161 | 162 | ### option.nextPageOrigin 163 | 164 | `option.nextPageOrigin` is yet another form of `option.next_page_object` with different data structure. 165 | 166 | ```javascript 167 | { origin: 'b.md', path: 'b.html', text: 'Title B' } 168 | ``` 169 | 170 | ### option.next_page 171 | 172 | `option.next_page` is a HTML string converted from `option.next_page_object`. 173 | 174 | ### option.relative_root_path 175 | 176 | `option.relative_root_path` is the relative path to the root path for the current page. 177 | 178 | For example, if the root path is `/` and the current page is `dir1/a.md`, `option.relative_root_path` is `../`. 179 | 180 | ### option.build_time 181 | 182 | `option.build_time` is the time of building the current page, which is a JavaScript Date instance. 183 | 184 | ### option.breadcrumbOrigin 185 | 186 | `option.breadcrumbOrigin` is an array containing the location information of current page. 187 | 188 | For example, if current page is `dir1/dir2/a.md`, `option.breadcrumb` is the following. 189 | 190 | ```javascript 191 | [ 192 | { path: 'index.html', text: 'Home' }, 193 | { path: 'dir1/index.html', text: 'dir1' }, 194 | { path: 'dir1/dir2/index.html', text: 'dir2' }, 195 | { path: 'dir1/dir2/a.html', text: 'Title A' } 196 | ] 197 | ``` 198 | 199 | ### option.breadcrumb 200 | 201 | `option.breadcrumb` is a HTML string which containing the location information of current page. 202 | 203 | ```html 204 | 210 | ``` 211 | 212 | ### option.toc 213 | 214 | `option.toc` is the table of content of current page. 215 | 216 | -------------------------------------------------------------------------------- /lib/chapters.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const fs = require('fs-extra'); 4 | const path = require('path'); 5 | const yaml = require('js-yaml'); 6 | const debug = require('debug')('[' + __filename + ']'); 7 | const { log } = require('./utils'); 8 | const walkSync = require('walk-sync'); 9 | const readmeCheck = require('../utils/readmeCheck'); 10 | const makeChaptersOrigin = require('../utils/makeChaptersOrigin'); 11 | const escapeTitle = require('../utils/escapeTitle'); 12 | 13 | const CHAPTERS_FILE = 'chapters.yml'; 14 | 15 | function mkFileHandler(prefix) { 16 | return function (file) { 17 | // handle directory name 18 | if (file[file.length - 1] === '/') { 19 | const obj = {}; 20 | const key = file; 21 | const value = path.basename(file); 22 | obj[key] = value; 23 | return obj; 24 | } 25 | 26 | // handle markdown file 27 | const filePath = path.join(path.resolve(process.cwd(), prefix), file); 28 | const fileContent = fs.readFileSync(filePath, 'utf8'); 29 | const h1Regex = /^\s*#\s*([^#].*?)\s*$/m; 30 | const result = h1Regex.exec(fileContent); 31 | const obj = {}; 32 | if (result && result[1]) { 33 | const key = file; 34 | const value = result[1]; 35 | obj[key] = value; 36 | } else { 37 | const key = file; 38 | const value = path.basename(file); 39 | obj[key] = value; 40 | } 41 | return obj; 42 | }; 43 | } 44 | 45 | function excludeEmptyDirectory(dirArr) { 46 | let hasEmptyDirectory = false; 47 | dirArr = dirArr.filter(function (d, i, arr) { 48 | if (i === (arr.length - 1)) { 49 | if (d.substr(-1) === '/') { 50 | hasEmptyDirectory = true; 51 | return false; 52 | } 53 | return true; 54 | } 55 | if (d.substr(-3).toLowerCase() === '.md') { 56 | return true; 57 | } 58 | if (arr[i + 1].indexOf(d) !== 0) { 59 | hasEmptyDirectory = true; 60 | return false; 61 | } 62 | return true; 63 | }); 64 | if (hasEmptyDirectory) { 65 | dirArr = excludeEmptyDirectory(dirArr); 66 | } 67 | return dirArr; 68 | } 69 | 70 | function chapters(option) { 71 | log.success('[BEGIN] handle chapters file'); 72 | 73 | // find whether user's chapters file exists 74 | let isChaptersExists; 75 | try { 76 | fs.accessSync(path.resolve(process.cwd(), CHAPTERS_FILE)); 77 | isChaptersExists = true; 78 | } catch (e) { 79 | isChaptersExists = false; 80 | } 81 | 82 | debug('does chapters file exists? ' + isChaptersExists); 83 | 84 | // if exists, read it, then return 85 | let chapters_content; 86 | let chapters_array; 87 | if (isChaptersExists) { 88 | try { 89 | chapters_content = fs.readFileSync(path.resolve(process.cwd(), CHAPTERS_FILE), 'utf8'); 90 | chapters_array = escapeTitle(yaml.load(chapters_content)); 91 | } catch (e) { 92 | log.error(e); 93 | throw e; 94 | } 95 | 96 | /* 97 | if (!chapters_array || chapters_array.length === 0) { 98 | log.error('The chapters file is invalid'); 99 | throw new Error('The chapters file is invalid'); 100 | } 101 | */ 102 | 103 | debug('chapter file %o', chapters_array); 104 | option.chapters = chapters_array; 105 | option.chaptersOrigin = makeChaptersOrigin(chapters_array); 106 | log.success('[END] handle chapters file'); 107 | return option; 108 | } 109 | 110 | // check if the doc dir exists 111 | const isDocDirExisted = fs.existsSync(path.resolve(process.cwd(), option.dir)); 112 | const isReadmeExisted = readmeCheck(process.cwd()); 113 | 114 | if (!isDocDirExisted && !isReadmeExisted) { 115 | log.error('[ERROR] cannot find any document source files'); 116 | process.exit(1); 117 | } 118 | 119 | let doc_array = []; 120 | 121 | if (isDocDirExisted) { 122 | // read the files and directories under the doc dir into an array 123 | let doc_items = walkSync(path.resolve(process.cwd(), option.dir), { ignore: ['node_modules'] }); 124 | doc_items = doc_items.filter(function (i) { 125 | if (i[0] === '.' || i[0] === '_') { 126 | return false; 127 | } 128 | return true; 129 | }).filter(function (i) { 130 | // if (i.substr(-9).toLowerCase() === 'readme.md') return false; 131 | if (i.substr(-9) === '/index.md') return false; 132 | if (i.substr(-3) === '.md') return true; 133 | if (i[i.length - 1] === '/') return true; 134 | return false; 135 | }); 136 | 137 | /* exclude empty directories from doc_items */ 138 | doc_items = excludeEmptyDirectory(doc_items); 139 | 140 | // read markdown file's title 141 | doc_array = escapeTitle(doc_items.map(mkFileHandler(option.dir))); 142 | } 143 | 144 | try { 145 | fs.writeFileSync( 146 | path.resolve(process.cwd(), CHAPTERS_FILE), 147 | yaml.dump(doc_array), 148 | 'utf8' 149 | ); 150 | log.info('[INFO] create the chapters file'); 151 | } catch (e) { 152 | log.error(e); 153 | throw e; 154 | } 155 | option.chapters = doc_array; 156 | option.chaptersOrigin = makeChaptersOrigin(doc_array); 157 | log.success('[END] handle chapters file'); 158 | return option; 159 | } 160 | 161 | module.exports = chapters; 162 | 163 | -------------------------------------------------------------------------------- /lib/config.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const fs = require('fs-extra'); 4 | const path = require('path'); 5 | const yaml = require('js-yaml'); 6 | const debug = require('debug')('[' + __filename + ']'); 7 | const { log } = require('./utils'); 8 | 9 | const CONFIG_FILE = 'loppo.yml'; 10 | const CONFIG_DEFAULT = '../loppo.yml.default'; 11 | 12 | function config(option) { 13 | log.success('[BEGIN] handle config file'); 14 | 15 | // find whether user's config file exists 16 | let isConfigExists; 17 | try { 18 | fs.accessSync(path.resolve(process.cwd(), CONFIG_FILE)); 19 | isConfigExists = true; 20 | } catch (e) { 21 | isConfigExists = false; 22 | } 23 | 24 | debug('does config file exists? ' + isConfigExists); 25 | 26 | // read config file 27 | let cfg; 28 | let cfg_content; 29 | if (!isConfigExists) { 30 | cfg_content = fs.readFileSync(path.resolve(__dirname, CONFIG_DEFAULT), 'utf8'); 31 | } else { 32 | cfg_content = fs.readFileSync(path.resolve(process.cwd(), CONFIG_FILE), 'utf8'); 33 | } 34 | try { 35 | cfg = yaml.load(cfg_content); 36 | debug('config file %o', cfg); 37 | } catch (e) { 38 | log.error(e); 39 | throw e; 40 | } 41 | 42 | // merge option into config 43 | if (option.dir && option.dir !== 'docs') cfg.dir = option.dir; 44 | if (option.output && option.output !== 'dist') cfg.output = option.output; 45 | if (option.site && option.site !== 'Documents') cfg.site = option.site; 46 | if (option.direction) cfg.direction = option.direction; 47 | if (option.theme) cfg.theme = option.theme; 48 | if (option.id) cfg.id = option.id; 49 | debug('config object %o', cfg); 50 | 51 | // If config file doesn't exist, copy the defalut 52 | if (!isConfigExists) { 53 | try { 54 | /* 55 | fs.copySync( 56 | path.resolve(__dirname, CONFIG_DEFAULT), 57 | path.resolve(process.cwd(), CONFIG_FILE) 58 | ); 59 | */ 60 | fs.writeFileSync( 61 | path.resolve(process.cwd(), CONFIG_FILE), 62 | yaml.dump(cfg), 63 | 'utf8' 64 | ); 65 | log.info('[INFO] create the config file'); 66 | } catch (e) { 67 | log.error(e); 68 | throw e; 69 | } 70 | } 71 | 72 | // add loppo_version into option 73 | const pkg = require(path.join(__dirname, '../package.json')); 74 | cfg.loppo_version = pkg.version; 75 | 76 | log.success('[END] handle config file'); 77 | 78 | return cfg; 79 | } 80 | 81 | module.exports = config; 82 | -------------------------------------------------------------------------------- /lib/copy.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const fs = require('fs-extra'); 4 | const path = require('path'); 5 | // const debug = require('debug')('[' + __filename + ']'); 6 | const { log } = require('./utils'); 7 | const themePath = require('../utils/themePath'); 8 | 9 | function copy(option) { 10 | log.success('[BEGIN] copy assets to output directory'); 11 | fs.removeSync(path.resolve(process.cwd(), option.output)); 12 | 13 | if (option.customization === undefined) { 14 | fs.copySync( 15 | path.resolve(process.cwd(), 'themes', option.theme), 16 | path.resolve(process.cwd(), option.output), 17 | { 18 | filter(filePath) { 19 | return !/\.template$/.test(filePath); 20 | } 21 | } 22 | ); 23 | } 24 | 25 | if (option.customization === false) { 26 | fs.copySync( 27 | themePath(option.theme), 28 | path.resolve(process.cwd(), option.output), 29 | { 30 | filter(filePath) { 31 | return !/\.template$/.test(filePath); 32 | } 33 | } 34 | ); 35 | } 36 | 37 | if (option.customization === true) { 38 | fs.copySync( 39 | path.resolve(process.cwd(), option.themeDir), 40 | path.resolve(process.cwd(), option.output), 41 | { 42 | filter(filePath) { 43 | return !/\.template$/.test(filePath); 44 | } 45 | } 46 | ); 47 | } 48 | 49 | // copy images sub directory 50 | if (fs.existsSync(path.resolve(process.cwd(), option.dir, 'images'))) { 51 | fs.ensureDirSync(path.resolve(process.cwd(), option.output, 'images')); 52 | fs.copySync( 53 | path.resolve(process.cwd(), option.dir, 'images'), 54 | path.resolve(process.cwd(), option.output, 'images'), 55 | {} 56 | ); 57 | } 58 | log.success('[END] copy assets to output directory'); 59 | return option; 60 | } 61 | 62 | module.exports = copy; 63 | -------------------------------------------------------------------------------- /lib/index.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const config = require('./config'); 4 | const chapters = require('./chapters'); 5 | const theme = require('./theme'); 6 | const copy = require('./copy'); 7 | const page = require('./page'); 8 | const debug = require('debug')('[' + __filename + ']'); 9 | const { log } = require('./utils'); 10 | 11 | function loppo(option) { 12 | log.success('[BEGIN] build docs'); 13 | debug('command line arguments %o', option); 14 | option = config(option); 15 | option = chapters(option); 16 | option = theme(option); 17 | option = copy(option); 18 | page(option); 19 | log.success('[END] build docs'); 20 | return option; 21 | } 22 | 23 | module.exports = loppo; 24 | -------------------------------------------------------------------------------- /lib/log4js.config.json: -------------------------------------------------------------------------------- 1 | { 2 | "appenders": { "out": { "type": "stdout", "layout": { "type": "pattern", "pattern": "%[%4p%] [%r] - %m" } } }, 3 | "categories": { "default": { "appenders": ["out"], "level": "info" } } 4 | } 5 | -------------------------------------------------------------------------------- /lib/page.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | // const fs = require('fs-extra'); 4 | // const path = require('path'); 5 | const debug = require('debug')('[' + __filename + ']'); 6 | const { log } = require('./utils'); 7 | const { makePage } = require('./utils'); 8 | 9 | function page(option) { 10 | log.success('[BEGIN] publish pages to output directory'); 11 | makePage('/', option); 12 | option.chapters.forEach(function (pageObj) { 13 | makePage(Object.keys(pageObj)[0], option); 14 | }); 15 | debug('makePage succeed'); 16 | log.success('[END] publish pages to output directory'); 17 | return option; 18 | } 19 | 20 | module.exports = page; 21 | -------------------------------------------------------------------------------- /lib/theme.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const fs = require('fs-extra'); 4 | const path = require('path'); 5 | const debug = require('debug')('[' + __filename + ']'); 6 | const { log } = require('./utils'); 7 | const walkSync = require('walk-sync'); 8 | const tmplEngine = require('tarim'); 9 | const themePath = require('../utils/themePath'); 10 | 11 | const THEMEDIR = 'loppo-theme'; 12 | 13 | /* 14 | // supports <% includes xxx %> syntax 15 | function includesHandler(templates) { 16 | const reg = /<%\s+includes\s+(.*?)\s+%>/g; 17 | const includesArr = []; 18 | while (true) { 19 | const match = reg.exec(templates.page); 20 | if (!match) break; 21 | includesArr.push(match); 22 | } 23 | includesArr.forEach(function (item) { 24 | templates.page = templates.page.replace(item[0], templates[item[1]]); 25 | }); 26 | return templates; 27 | } 28 | */ 29 | 30 | function readTemplate(option, dest) { 31 | const pathArr = walkSync(path.resolve(dest), { 32 | globs: ['*.template'], 33 | directories: false 34 | }); 35 | 36 | let include_theme_dir; 37 | switch (option.customization) { 38 | case undefined: 39 | include_theme_dir = 'themes' + path.sep + option.theme; 40 | break; 41 | case false: 42 | include_theme_dir = dest; 43 | break; 44 | case true: 45 | include_theme_dir = option.themeDir ? option.themeDir : THEMEDIR; 46 | break; 47 | default: 48 | break; 49 | } 50 | 51 | pathArr.forEach(function (p) { 52 | const pageTemplate = fs.readFileSync(path.resolve(dest, p), 'utf8'); 53 | if (!option.templates) option.templates = {}; 54 | option.templates[p.substr(0, p.length - 9)] = tmplEngine( 55 | pageTemplate, 56 | { 57 | includePath: include_theme_dir, 58 | includeExt: '.template' 59 | } 60 | ); 61 | // option.templates[p.substr(0, p.length - 9)] = _.template(pageTemplate); 62 | }); 63 | /* 64 | const pageTemplate = fs.readFileSync(path.resolve(dest, 'page.template'), 'utf8'); 65 | option.templates = { 66 | page: _.template(pageTemplate) 67 | }; 68 | */ 69 | // option.templates = includesHandler(option.templates); 70 | // option.templates.page = _.template(option.templates.page); 71 | debug('read templates succeed'); 72 | 73 | return option; 74 | } 75 | 76 | function compatibleEarlyVersion(option) { 77 | // find whether the theme file exists 78 | let isThemeExists; 79 | const THEME_DIR = 'themes' + path.sep + option.theme; 80 | 81 | try { 82 | fs.accessSync(path.resolve(process.cwd(), THEME_DIR)); 83 | isThemeExists = true; 84 | } catch (e) { 85 | isThemeExists = false; 86 | } 87 | 88 | debug('does theme file exists? ' + isThemeExists); 89 | 90 | if (isThemeExists) { 91 | option = readTemplate(option, path.resolve(process.cwd(), THEME_DIR)); 92 | return option; 93 | } 94 | 95 | const theme_src = themePath(option.theme); 96 | const theme_dest = path.resolve(process.cwd(), 'themes', option.theme); 97 | // fs.ensureDirSync(theme_dest); 98 | fs.copySync(theme_src, theme_dest); 99 | 100 | debug('Copying theme directory succeed'); 101 | 102 | return readTemplate(option, theme_dest); 103 | } 104 | 105 | function customizeTheme(option) { 106 | const absoluteThemePath = path.resolve(process.cwd(), THEMEDIR); 107 | if (fs.existsSync(absoluteThemePath)) { 108 | return readTemplate(option, absoluteThemePath); 109 | } 110 | 111 | const oldThemeDir = path.resolve(process.cwd(), 'themes' + path.sep + option.theme); 112 | 113 | if (fs.existsSync(oldThemeDir)) { 114 | fs.moveSync(oldThemeDir, absoluteThemePath); 115 | return readTemplate(option, absoluteThemePath); 116 | } 117 | 118 | let theme_src = ''; 119 | try { 120 | theme_src = require('loppo-theme-' + option.theme); 121 | } catch (e) { 122 | theme_src = require(path.resolve(__dirname, '../node_modules', 'loppo-theme-' + option.theme)); 123 | } 124 | fs.copySync(theme_src, absoluteThemePath); 125 | return readTemplate(option, absoluteThemePath); 126 | } 127 | 128 | function originalTheme(option) { 129 | const theme_src = themePath(option.theme); 130 | return readTemplate(option, theme_src); 131 | } 132 | 133 | function theme(option) { 134 | log.success('[BEGIN] handle theme files'); 135 | const { customization } = option; 136 | 137 | // supports earlier versions before v0.6 138 | if (customization === undefined) { 139 | debug('option.customization is not existed'); 140 | option = compatibleEarlyVersion(option); 141 | } 142 | 143 | if (customization === true) { 144 | debug('option.customization is true'); 145 | option = customizeTheme(option); 146 | } 147 | 148 | if (customization === false) { 149 | debug('option.customization is false'); 150 | option = originalTheme(option); 151 | } 152 | 153 | log.success('[END] handle theme files'); 154 | return option; 155 | } 156 | 157 | module.exports = theme; 158 | -------------------------------------------------------------------------------- /lib/utils.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const path = require('path'); 4 | const fs = require('fs-extra'); 5 | const debug = require('debug')('[' + __filename + ']'); 6 | const _ = require('lodash'); 7 | const md = require('turpan'); 8 | 9 | /* 10 | * log() 11 | */ 12 | const log4js = require('log4js'); 13 | log4js.configure(path.resolve(__dirname, './log4js.config.json')); 14 | const logSymbols = require('log-symbols'); 15 | const log = log4js.getLogger('loppo'); 16 | const logWithIcon = {}; 17 | 18 | function noop() {} 19 | 20 | logWithIcon.info = function info() { 21 | process.stdout.write(logSymbols.info + ' '); 22 | log.info.apply(log, arguments); 23 | }; 24 | 25 | logWithIcon.success = function success() { 26 | process.stdout.write(logSymbols.success + ' '); 27 | log.info.apply(log, arguments); 28 | }; 29 | 30 | logWithIcon.warn = function warn() { 31 | process.stdout.write(logSymbols.warning + ' '); 32 | log.warn.apply(log, arguments); 33 | }; 34 | 35 | logWithIcon.error = function error() { 36 | process.stdout.write(logSymbols.error + ' '); 37 | log.error.apply(log, arguments); 38 | }; 39 | 40 | logWithIcon.fatal = function fatal() { 41 | process.stdout.write(logSymbols.error + ' '); 42 | log.fatal.apply(log, arguments); 43 | }; 44 | 45 | logWithIcon.setLevel = function setLevel() { 46 | // log.setLevel.call(log, arguments[0]); 47 | log.level = arguments[0]; 48 | if (arguments[0].toLowerCase() === 'warn') { 49 | logWithIcon.info = noop; 50 | logWithIcon.success = noop; 51 | } 52 | if (arguments[0].toLowerCase() === 'error') { 53 | logWithIcon.info = noop; 54 | logWithIcon.success = noop; 55 | logWithIcon.warn = noop; 56 | } 57 | if (arguments[0].toLowerCase() === 'fatal') { 58 | logWithIcon.info = noop; 59 | logWithIcon.success = noop; 60 | logWithIcon.warn = noop; 61 | logWithIcon.fatal = noop; 62 | } 63 | }; 64 | 65 | exports.log = logWithIcon; 66 | 67 | 68 | /* 69 | * makeContent() 70 | */ 71 | 72 | function makeContent(root, optionObj) { 73 | // if a regular markdown file 74 | if (root[root.length - 1] !== '/') { 75 | const filePath = path.resolve(process.cwd(), optionObj.dir, root); 76 | optionObj.content = fs.readFileSync(filePath, 'utf8'); 77 | return optionObj; 78 | } 79 | 80 | // if root directory 81 | if (root === '/') { 82 | // check if readme.md exists 83 | const readme1 = path.resolve(process.cwd(), 'readme.md'); 84 | const readme2 = path.resolve(process.cwd(), 'README.MD'); 85 | const readme3 = path.resolve(process.cwd(), 'README.md'); 86 | if (fs.existsSync(readme1)) { 87 | optionObj.content = fs.readFileSync(readme1, 'utf8'); 88 | } else if (fs.existsSync(readme2)) { 89 | optionObj.content = fs.readFileSync(readme2, 'utf8'); 90 | } else if (fs.existsSync(readme3)) { 91 | optionObj.content = fs.readFileSync(readme3, 'utf8'); 92 | } else { 93 | optionObj.content = ''; 94 | } 95 | 96 | // modify images position 97 | const reg = new RegExp('\\(' + optionObj.dir + '\\/images\\/', 'g'); 98 | optionObj.content = optionObj.content.replace(reg, '(images/'); 99 | 100 | // if sub directory with index.md 101 | } else if (fs.existsSync(path.resolve(process.cwd(), optionObj.dir, root, 'index.md'))) { 102 | optionObj.content = fs.readFileSync( 103 | path.posix.resolve(process.cwd(), optionObj.dir, root, 'index.md'), 104 | 'utf8' 105 | ); 106 | // if sub directory without index.md 107 | } else { 108 | const chapters = optionObj.chapters 109 | .filter(p => Object.keys(p)[0].indexOf(root) === 0) 110 | .filter((p) => { 111 | const pathArr = path.posix.relative(root, Object.keys(p)[0]).split('/'); 112 | if (pathArr.length > 1) return false; 113 | if (pathArr[0] === '') return false; 114 | return true; 115 | }); 116 | let contentStr = ''; 117 | chapters.forEach((p) => { 118 | const pagePath = Object.keys(p)[0]; 119 | const relativePath = path.posix.relative(root, pagePath); 120 | let pageHtml = ''; 121 | if (relativePath.substr(-3).toLowerCase() === '.md') { 122 | pageHtml = relativePath.substr(0, relativePath.length - 3) + '.html'; 123 | } else { 124 | pageHtml = relativePath + '/'; 125 | } 126 | const pageName = p[pagePath]; 127 | contentStr += '- [' + pageName + ']'; 128 | contentStr += '(' + pageHtml + ')\n'; 129 | }); 130 | optionObj.content = contentStr; 131 | } 132 | return optionObj; 133 | } 134 | 135 | exports.makeContent = makeContent; 136 | 137 | /* 138 | * findRootPosition() 139 | */ 140 | function findRootPosition(root, optionObj) { 141 | for (let i = 0; i < optionObj.chapters.length; i += 1) { 142 | if (optionObj.chapters[i][root]) { 143 | return i; 144 | } 145 | } 146 | throw new Error('Cannot find the path in Chapters!'); 147 | } 148 | 149 | 150 | /* 151 | * makePageTitle() 152 | */ 153 | function makePageTitle(root, optionObj) { 154 | if (root.substr(-3).toLowerCase() === '.md') { 155 | if (optionObj.content) { 156 | const contentArr = optionObj.content.split('\n'); 157 | const hasTitle = /^#\s*(.*)\s*$/.exec(contentArr[0]); 158 | if (hasTitle) { 159 | optionObj.page_title = _.escape(hasTitle[1]); 160 | contentArr.shift(); 161 | optionObj.content = contentArr.join('\n'); 162 | } else { 163 | const position = findRootPosition(root, optionObj); 164 | optionObj.page_title = optionObj.chapters[position][root]; 165 | } 166 | } else { 167 | const position = findRootPosition(root, optionObj); 168 | optionObj.page_title = optionObj.chapters[position][root]; 169 | } 170 | return optionObj; 171 | } 172 | 173 | // if sub-directory 174 | if (root[root.length - 1] === '/' && root !== '/') { 175 | const contentArr = optionObj.content.split('\n'); 176 | const hasTitle = /^#\s*(.*)$/.exec(contentArr[0]); 177 | if (hasTitle) { 178 | optionObj.page_title = _.escape(hasTitle[1]); 179 | contentArr.shift(); 180 | optionObj.content = contentArr.join('\n'); 181 | } else { 182 | const position = findRootPosition(root, optionObj); 183 | optionObj.page_title = optionObj.chapters[position][root]; 184 | } 185 | return optionObj; 186 | } 187 | 188 | // if root directory 189 | if (root === '/') { 190 | if (optionObj.content !== '') { 191 | const contentArr = optionObj.content.split('\n'); 192 | const hasTitle = /^#\s*(.*)$/.exec(contentArr[0]); 193 | if (hasTitle) { 194 | optionObj.page_title = _.escape(hasTitle[1]); 195 | contentArr.shift(); 196 | optionObj.content = contentArr.join('\n'); 197 | } else { 198 | optionObj.page_title = optionObj.site; 199 | } 200 | } else { 201 | optionObj.page_title = optionObj.site; 202 | } 203 | return optionObj; 204 | } 205 | 206 | optionObj.page_title = optionObj.site; 207 | return optionObj; 208 | } 209 | 210 | exports.makePageTitle = makePageTitle; 211 | 212 | 213 | /* 214 | * makePreviousPageObject() 215 | */ 216 | const makePreviousPageObject = require('../utils/makePreviousPageObject'); 217 | exports.makePreviousPageObject = makePreviousPageObject; 218 | 219 | /* 220 | * makePreviousPageOrigin() 221 | */ 222 | const makePreviousPageOrigin = require('../utils/makePreviousPageOrigin'); 223 | exports.makePreviousPageOrigin = makePreviousPageOrigin; 224 | 225 | /* 226 | * makeNextPageObject() 227 | */ 228 | const makeNextPageObject = require('../utils/makeNextPageObject'); 229 | exports.makeNextPageObject = makeNextPageObject; 230 | 231 | /* 232 | * makeNextPageOrigin() 233 | */ 234 | const makeNextPageOrigin = require('../utils/makeNextPageOrigin'); 235 | exports.makeNextPageOrigin = makeNextPageOrigin; 236 | 237 | /* 238 | * makeRelativeRootPath() 239 | */ 240 | function makeRelativeRootPath(root, optionObj) { 241 | if (root === '/') { 242 | optionObj.relative_root_path = './'; 243 | } else { 244 | const pathArr = root.split('/'); 245 | optionObj.relative_root_path = _.repeat('../', pathArr.length - 1); 246 | } 247 | return optionObj; 248 | } 249 | 250 | exports.makeRelativeRootPath = makeRelativeRootPath; 251 | 252 | 253 | /* 254 | * makePreviousPage() 255 | */ 256 | function makePreviousPage(root, optionObj) { 257 | const pp = optionObj.previous_page_object; 258 | if (pp === null) { 259 | // optionObj.previous_page = ''; 260 | optionObj.previous_page = '' 261 | + '' 262 | + ''; 263 | } else { 264 | let pp_path = Object.keys(pp)[0]; 265 | if (pp_path.substr(-1) === '/') { 266 | pp_path += 'index.html'; 267 | } else if (pp_path.substr(-3) === '.md') { 268 | pp_path = pp_path.substr(0, pp_path.length - 3) + '.html'; 269 | } 270 | optionObj.previous_page = '' 274 | + '' 275 | + '« Previous'; 276 | } 277 | return optionObj; 278 | } 279 | 280 | exports.makePreviousPage = makePreviousPage; 281 | 282 | 283 | /* 284 | * makeNextPage() 285 | */ 286 | function makeNextPage(root, optionObj) { 287 | const np = optionObj.next_page_object; 288 | if (np === null) { 289 | // optionObj.next_page = ''; 290 | optionObj.next_page = ''; 293 | } else { 294 | let np_path = Object.keys(np)[0]; 295 | if (np_path.substr(-1) === '/') { 296 | np_path += 'index.html'; 297 | } else if (np_path.substr(-3) === '.md') { 298 | np_path = np_path.substr(0, np_path.length - 3) + '.html'; 299 | } 300 | optionObj.next_page = ''; 307 | } 308 | return optionObj; 309 | } 310 | 311 | exports.makeNextPage = makeNextPage; 312 | 313 | 314 | /* 315 | * makeBuildTime() 316 | */ 317 | function makeBuildTime(root, optionObj) { 318 | optionObj.build_time = new Date(); 319 | return optionObj; 320 | } 321 | 322 | exports.makeBuildTime = makeBuildTime; 323 | 324 | /* 325 | * makeBreadcrumbOrigin() 326 | */ 327 | const makeBreadcrumbOrigin = require('../utils/makeBreadcrumbOrigin') 328 | exports.makeBreadcrumbOrigin = makeBreadcrumbOrigin; 329 | 330 | /* 331 | * makeBreadcrumb() 332 | */ 333 | const makeBreadcrumb = require('../utils/makeBreadcrumb'); 334 | exports.makeBreadcrumb = makeBreadcrumb; 335 | 336 | /* 337 | * makeChapterList 338 | */ 339 | const makeChapterList = require('../utils/makeChapterList'); 340 | exports.makeChapterList = makeChapterList; 341 | 342 | /* 343 | * makeMarkdown() 344 | */ 345 | function markdownRender(root, optionObj) { 346 | md.set({ 347 | tocCallback(tocMarkdown, tocArray, tocHtml) { 348 | optionObj.toc = tocHtml; 349 | } 350 | }); 351 | 352 | optionObj.content = md.render(optionObj.content); 353 | return optionObj; 354 | } 355 | 356 | exports.markdownRender = markdownRender; 357 | 358 | 359 | /* 360 | * writePage() 361 | */ 362 | const minify = require('html-minifier').minify; 363 | function writePage(root, optionObj) { 364 | const pageContent = optionObj.templates.page(optionObj); 365 | let relativePath = root; 366 | if (relativePath === '/') { 367 | relativePath = './'; 368 | } 369 | if (relativePath[relativePath.length - 1] === '/') { 370 | relativePath += 'index.html'; 371 | } else { 372 | relativePath = relativePath.substr(0, relativePath.length - 3) + '.html'; 373 | } 374 | fs.outputFileSync( 375 | path.resolve( 376 | process.cwd(), 377 | optionObj.output, 378 | relativePath 379 | ), 380 | minify(pageContent, { 381 | minifyJS: true, 382 | collapseWhitespace: true, 383 | }) 384 | ); 385 | } 386 | 387 | exports.writePage = writePage; 388 | 389 | /* 390 | * isHomepage() 391 | */ 392 | const isHomepage = require('../utils/isHomepage'); 393 | exports.isHomepage = isHomepage; 394 | 395 | /* 396 | * makePage() 397 | */ 398 | function makePage(root, optionObj) { 399 | logWithIcon.success('[BEGIN] create ' + root + ' page'); 400 | 401 | // create option.current_path 402 | optionObj.current_path = root; 403 | debug('get current_path: ', optionObj.current_path); 404 | 405 | // create option.isHomepage 406 | optionObj.isHomepage = isHomepage(root); 407 | debug('get isHomepage: ', optionObj.isHomepage); 408 | 409 | // create option.content 410 | optionObj = makeContent(root, optionObj); 411 | debug('get page content: ', optionObj.content); 412 | 413 | // create option.page_title 414 | optionObj = makePageTitle(root, optionObj); 415 | debug('get page title: ', optionObj.page_title); 416 | 417 | // create option.previous_page_object 418 | optionObj = makePreviousPageObject(root, optionObj); 419 | debug('get previous page object: ', optionObj.previous_page_object); 420 | 421 | // create option.previousPageOrigin 422 | optionObj = makePreviousPageOrigin(root, optionObj); 423 | debug('get previousPageOrigin: ', optionObj.previousPageOrigin); 424 | 425 | // create option.next_page_object 426 | optionObj = makeNextPageObject(root, optionObj); 427 | debug('get next page object: ', optionObj.next_page_object); 428 | 429 | // create option.nextPageOrigin 430 | optionObj = makeNextPageOrigin(root, optionObj); 431 | debug('get nextPageOrigin: ', optionObj.nextPageOrigin); 432 | 433 | // create option.relative_root_path 434 | optionObj = makeRelativeRootPath(root, optionObj); 435 | debug('get relative root path: ', optionObj.relative_root_path); 436 | 437 | // create option.previous_page 438 | optionObj = makePreviousPage(root, optionObj); 439 | debug('get previous page: ', optionObj.previous_page); 440 | 441 | // create option.next_page 442 | optionObj = makeNextPage(root, optionObj); 443 | debug('get next page: ', optionObj.next_page); 444 | 445 | // create option.build_time 446 | optionObj = makeBuildTime(root, optionObj); 447 | debug('get build time: ', optionObj.build_time); 448 | 449 | // create option.breadcrumbOrigin 450 | optionObj = makeBreadcrumbOrigin(root, optionObj); 451 | debug('get breadcrumbOrigin: ', optionObj.breadcrumbOrigin); 452 | 453 | // create option.breadcrumb 454 | optionObj = makeBreadcrumb(root, optionObj); 455 | debug('get breadcrumb: ', optionObj.breadcrumb); 456 | 457 | // create option.chapterList 458 | optionObj = makeChapterList(root, optionObj); 459 | debug('get chapterList: ', optionObj.chapterList); 460 | 461 | // create option.toc and option.content in HTML 462 | optionObj = markdownRender(root, optionObj); 463 | debug('get option.toc: ', optionObj.toc); 464 | debug('get option.content in HTML: ', optionObj.content); 465 | 466 | // write to page 467 | writePage(root, optionObj); 468 | 469 | logWithIcon.success('[END] create ' + root + ' page'); 470 | } 471 | 472 | exports.makePage = makePage; 473 | 474 | -------------------------------------------------------------------------------- /loppo.yml: -------------------------------------------------------------------------------- 1 | dir: docs 2 | output: dist 3 | site: Documents 4 | theme: oceandeep 5 | customization: false 6 | themeDir: loppo-theme 7 | direction: ltr 8 | id: loppo 9 | -------------------------------------------------------------------------------- /loppo.yml.default: -------------------------------------------------------------------------------- 1 | # directory of document source files 2 | dir: docs 3 | 4 | # output directory of generated documents 5 | output: dist 6 | 7 | # site name 8 | site: Documents 9 | 10 | # site theme 11 | theme: oceandeep 12 | 13 | # use customized theme 14 | customization: false 15 | 16 | # dir to put customized theme in your project 17 | themeDir: loppo-theme 18 | 19 | # site direction 20 | direction: LTR 21 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "loppo", 3 | "version": "0.6.26", 4 | "description": "an extremely easy static site generator of markdown documents", 5 | "main": "index.js", 6 | "scripts": { 7 | "lint": "eslint './**/*.@(js|jsx)'", 8 | "test": "tape test/*.test.js test/**/*.test.js | tap-spec", 9 | "commit": "git cz", 10 | "changelog": "conventional-changelog -p angular -i CHANGELOG.md -s -r 1" 11 | }, 12 | "bin": { 13 | "loppo": "./bin/cmd.js" 14 | }, 15 | "keywords": [ 16 | "static", 17 | "document" 18 | ], 19 | "homepage": "https://github.com/ruanyf/loppo", 20 | "repository": { 21 | "type": "git", 22 | "url": "https://github.com/ruanyf/loppo.git" 23 | }, 24 | "author": "Ruan Yifeng", 25 | "license": "GPL-3.0", 26 | "dependencies": { 27 | "connect": "^3.7.0", 28 | "debug": "^4.3.2", 29 | "fs-extra": "11.x", 30 | "html-minifier": "4.x", 31 | "html-to-text": "9.x", 32 | "js-yaml": "4.x", 33 | "lodash": "^4.17.21", 34 | "log-symbols": "4.x", 35 | "log4js": "^6.3.0", 36 | "loppo-theme-oceandeep": "2.x", 37 | "promptly": "^3.2.0", 38 | "serve-static": "^1.14.1", 39 | "tarim": "^0.1.4", 40 | "turpan": "^0.4.0", 41 | "walk-sync": "3.x", 42 | "wordcount": "^1.1.1", 43 | "yargs": "17.x" 44 | }, 45 | "devDependencies": { 46 | "@commitlint/cli": "18.x", 47 | "@commitlint/config-conventional": "18.x", 48 | "commitizen": "^4.2.4", 49 | "conventional-changelog-cli": "4.x", 50 | "cz-conventional-changelog": "3.x", 51 | "eslint": "8.x", 52 | "eslint-config-airbnb": "19.x", 53 | "eslint-plugin-import": "^2.24.2", 54 | "eslint-plugin-jsx-a11y": "^6.4.1", 55 | "eslint-plugin-react": "^7.25.1", 56 | "ghooks": "^2.0.4", 57 | "husky": "^4.3.8", 58 | "tap-spec": "^5.0.0", 59 | "tape": "^5.3.1" 60 | }, 61 | "husky": { 62 | "hooks": { 63 | "pre-commit": "npm test", 64 | "commit-msg": "commitlint -E HUSKY_GIT_PARAMS" 65 | } 66 | }, 67 | "config": { 68 | "validate-commit-msg": { 69 | "types": [ 70 | "feat", 71 | "fix", 72 | "docs", 73 | "style", 74 | "refactor", 75 | "perf", 76 | "test", 77 | "chore", 78 | "revert" 79 | ], 80 | "warnOnFail": false, 81 | "maxSubjectLength": 100 82 | }, 83 | "commitizen": { 84 | "path": "node_modules/cz-conventional-changelog" 85 | } 86 | } 87 | } 88 | -------------------------------------------------------------------------------- /test/chapters.test.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const path = require('path'); 4 | const fs = require('fs'); 5 | const test = require('tape'); 6 | const yaml = require('js-yaml'); 7 | const chapters = require('../lib/chapters'); 8 | 9 | const log = require('../lib/utils').log; 10 | log.setLevel('error'); 11 | 12 | const test_title = '[chapters.js] '; 13 | const CHAPTERS_FILE = 'chapters.yml'; 14 | 15 | test( 16 | test_title + 17 | 'if chapters.yml already exists, add the content to option.chapters', 18 | function (t) { 19 | const TEST_PATH = path.resolve(__dirname, './fixture/chapters/chapters-file-already-exists'); 20 | process.chdir(TEST_PATH); 21 | const result = chapters({}); 22 | t.equal(result.chapters[0]['a.md'], 'a'); 23 | t.equal(result.chapters[1]['b.md'], 'b'); 24 | t.end(); 25 | } 26 | ); 27 | 28 | test.skip( 29 | test_title + 30 | 'if no docs directory exists, it will print throw an error', 31 | function (t) { 32 | const TEST_PATH = path.resolve(__dirname, './fixture/chapters/no-docs-directory-exists'); 33 | process.chdir(TEST_PATH); 34 | t.throws(function () { 35 | chapters({ dir: 'docs' }); 36 | }); 37 | t.end(); 38 | } 39 | ); 40 | 41 | test( 42 | test_title + 43 | 'simple generating chapters.yml', 44 | function (t) { 45 | const TEST_PATH = path.resolve(__dirname, './fixture/chapters/simple-generating-chapters-file'); 46 | process.chdir(TEST_PATH); 47 | const result = chapters({ dir: 'docs' }); 48 | t.equal(result.chapters[0]['a.md'], 'Title A'); 49 | t.equal(result.chapters[1]['b.md'], 'Title B'); 50 | t.ok(fs.existsSync(path.join(TEST_PATH, CHAPTERS_FILE))); 51 | const content = yaml.load(fs.readFileSync(path.join(TEST_PATH, CHAPTERS_FILE), 'utf8')); 52 | t.equal(content[0]['a.md'], 'Title A'); 53 | t.equal(content[1]['b.md'], 'Title B'); 54 | t.end(); 55 | fs.unlinkSync(path.join(TEST_PATH, CHAPTERS_FILE)); 56 | } 57 | ); 58 | 59 | test( 60 | test_title + 61 | 'exclude empty directory', 62 | function (t) { 63 | const TEST_PATH = path.resolve(__dirname, './fixture/chapters/exclude-empty-directory'); 64 | process.chdir(TEST_PATH); 65 | const result = chapters({ dir: 'docs' }); 66 | t.equal(result.chapters[0]['a.md'], 'A'); 67 | t.equal(result.chapters.length, 1); 68 | t.ok(fs.existsSync(path.join(TEST_PATH, CHAPTERS_FILE))); 69 | const content = yaml.load(fs.readFileSync(path.join(TEST_PATH, CHAPTERS_FILE), 'utf8')); 70 | t.equal(content[0]['a.md'], 'A'); 71 | t.equal(content.length, 1); 72 | t.end(); 73 | fs.unlinkSync(path.join(TEST_PATH, CHAPTERS_FILE)); 74 | } 75 | ); 76 | 77 | test( 78 | test_title + 79 | 'exclude double empty directory', 80 | function (t) { 81 | const TEST_PATH = path.resolve(__dirname, './fixture/chapters/exclude-double-empty-directory'); 82 | process.chdir(TEST_PATH); 83 | const result = chapters({ dir: 'docs' }); 84 | t.equal(result.chapters[0]['a.md'], 'A'); 85 | t.equal(result.chapters.length, 1); 86 | t.ok(fs.existsSync(path.join(TEST_PATH, CHAPTERS_FILE))); 87 | const content = yaml.load(fs.readFileSync(path.join(TEST_PATH, CHAPTERS_FILE), 'utf8')); 88 | t.equal(content[0]['a.md'], 'A'); 89 | t.equal(content.length, 1); 90 | t.end(); 91 | fs.unlinkSync(path.join(TEST_PATH, CHAPTERS_FILE)); 92 | } 93 | ); 94 | 95 | test( 96 | test_title + 97 | 'exclude complex empty directory', 98 | function (t) { 99 | const TEST_PATH = path.resolve(__dirname, './fixture/chapters/exclude-complex-empty-directory'); 100 | process.chdir(TEST_PATH); 101 | const result = chapters({ dir: 'docs' }); 102 | t.equal(result.chapters[0]['a.md'], 'A'); 103 | t.equal(result.chapters[1]['images/'], 'images'); 104 | t.equal(result.chapters[2]['images/dir3/'], 'dir3'); 105 | t.equal(result.chapters[3]['images/dir3/b.md'], 'B'); 106 | t.equal(result.chapters.length, 4); 107 | t.ok(fs.existsSync(path.join(TEST_PATH, CHAPTERS_FILE))); 108 | const content = yaml.load(fs.readFileSync(path.join(TEST_PATH, CHAPTERS_FILE), 'utf8')); 109 | t.equal(content[0]['a.md'], 'A'); 110 | t.equal(content[1]['images/'], 'images'); 111 | t.equal(content[2]['images/dir3/'], 'dir3'); 112 | t.equal(content[3]['images/dir3/b.md'], 'B'); 113 | t.equal(content.length, 4); 114 | t.end(); 115 | fs.unlinkSync(path.join(TEST_PATH, CHAPTERS_FILE)); 116 | } 117 | ); 118 | 119 | test( 120 | test_title + 121 | 'generating chapters.yml filter', 122 | function (t) { 123 | const TEST_PATH = path.resolve(__dirname, './fixture/chapters/generating-chapters-file-filter'); 124 | process.chdir(TEST_PATH); 125 | chapters({ dir: 'docs' }); 126 | t.ok(fs.existsSync(path.join(TEST_PATH, CHAPTERS_FILE))); 127 | const content = yaml.load(fs.readFileSync(path.join(TEST_PATH, CHAPTERS_FILE), 'utf8')); 128 | t.equal(typeof content, 'object'); 129 | t.equal(content.length, 0); 130 | t.end(); 131 | fs.unlinkSync(path.join(TEST_PATH, CHAPTERS_FILE)); 132 | } 133 | ); 134 | 135 | test( 136 | test_title + 137 | 'empty chapters.yml', 138 | function (t) { 139 | const TEST_PATH = path.resolve( 140 | __dirname, 141 | './fixture/chapters/empty-chapters-file' 142 | ); 143 | process.chdir(TEST_PATH); 144 | const result = chapters({ dir: 'docs' }); 145 | t.equal(result.chapters, undefined); 146 | t.end(); 147 | } 148 | ); 149 | 150 | test( 151 | test_title + 152 | 'generating chapters.yml complex format', 153 | function (t) { 154 | const TEST_PATH = path.resolve( 155 | __dirname, 156 | './fixture/chapters/generating-chapters-file-complex-format' 157 | ); 158 | process.chdir(TEST_PATH); 159 | const result = chapters({ dir: 'docs' }); 160 | t.equal(result.chapters[0]['a.md'], 'Title A'); 161 | t.equal(result.chapters[1]['b.md'], 'Title B'); 162 | t.ok(fs.existsSync(path.join(TEST_PATH, CHAPTERS_FILE))); 163 | const content = yaml.load(fs.readFileSync(path.join(TEST_PATH, CHAPTERS_FILE), 'utf8')); 164 | t.equal(content[0]['a.md'], 'Title A'); 165 | t.equal(content[1]['b.md'], 'Title B'); 166 | t.end(); 167 | fs.unlinkSync(path.join(TEST_PATH, CHAPTERS_FILE)); 168 | } 169 | ); 170 | 171 | test( 172 | test_title + 173 | 'complex docs directory structure', 174 | function (t) { 175 | const TEST_PATH = path.resolve( 176 | __dirname, 177 | './fixture/chapters/complex-docs-directory-structure' 178 | ); 179 | process.chdir(TEST_PATH); 180 | const result = chapters({ dir: 'docs' }); 181 | t.equal(result.chapters[0]['a.md'], 'Title A'); 182 | t.equal(result.chapters[1]['b/'], 'b'); 183 | t.equal(result.chapters[2]['b/c/'], 'c'); 184 | t.equal(result.chapters[3]['b/c/d.md'], 'Title D'); 185 | t.ok(fs.existsSync(path.join(TEST_PATH, CHAPTERS_FILE))); 186 | const content = yaml.load(fs.readFileSync(path.join(TEST_PATH, CHAPTERS_FILE), 'utf8')); 187 | t.equal(content[0]['a.md'], 'Title A'); 188 | t.equal(content[1]['b/'], 'b'); 189 | t.equal(content[2]['b/c/'], 'c'); 190 | t.equal(content[3]['b/c/d.md'], 'Title D'); 191 | fs.unlinkSync(path.join(TEST_PATH, CHAPTERS_FILE)); 192 | t.end(); 193 | } 194 | ); 195 | -------------------------------------------------------------------------------- /test/config.test.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const path = require('path'); 4 | const fs = require('fs'); 5 | const test = require('tape'); 6 | const yaml = require('js-yaml'); 7 | const config = require('../lib/config'); 8 | 9 | const log = require('../lib/utils').log; 10 | log.setLevel('error'); 11 | 12 | const test_title = '[config.js] '; 13 | 14 | test( 15 | test_title + 16 | 'if no command line options and no existing config file, lib/config output should be the default', 17 | function (t) { 18 | const TEST_PATH = path.resolve(__dirname, './fixture/config/no-options-no-default'); 19 | process.chdir(TEST_PATH); 20 | const result = config({}); 21 | 22 | t.equal(result.dir, 'docs'); 23 | t.equal(result.output, 'dist'); 24 | t.equal(result.customization, false); 25 | t.ok(fs.existsSync(path.join(TEST_PATH, 'loppo.yml'))); 26 | t.end(); 27 | fs.unlinkSync(path.join(TEST_PATH, 'loppo.yml')); 28 | } 29 | ); 30 | 31 | test( 32 | test_title + 33 | 'if no command line options and an existing config file,' 34 | + ' lib/config output should be the existing config', 35 | function (t) { 36 | const TEST_PATH = path.resolve(__dirname, './fixture/config/no-options-with-config'); 37 | process.chdir(TEST_PATH); 38 | const result = config({}); 39 | t.equal(result.dir, 'aaa'); 40 | t.equal(result.output, 'bbb'); 41 | t.equal(result.theme, 'new'); 42 | t.equal(result.customization, undefined); 43 | t.end(); 44 | } 45 | ); 46 | 47 | test( 48 | test_title + 49 | 'if command line options and no existing config file, ' 50 | + 'lib/config output should be the command line options', 51 | function (t) { 52 | const TEST_PATH = path.resolve(__dirname, './fixture/config/options-no-config'); 53 | process.chdir(TEST_PATH); 54 | const result = config({ dir: 'aaa', output: 'bbb', theme: 'new' }); 55 | t.equal(result.dir, 'aaa'); 56 | t.equal(result.output, 'bbb'); 57 | t.ok(fs.existsSync(path.join(TEST_PATH, 'loppo.yml'))); 58 | const doc = yaml.load(fs.readFileSync(path.join(TEST_PATH, 'loppo.yml'), 'utf8')); 59 | t.equal(doc.dir, 'aaa'); 60 | t.equal(doc.output, 'bbb'); 61 | t.equal(doc.theme, 'new'); 62 | t.equal(doc.customization, false); 63 | t.end(); 64 | fs.unlinkSync(path.join(TEST_PATH, 'loppo.yml')); 65 | } 66 | ); 67 | 68 | test( 69 | test_title + 70 | 'if command line options and an existing config file, ' 71 | + 'lib/config output should be the command line options, ' 72 | + 'and the config file should be the same', 73 | function (t) { 74 | const TEST_PATH = path.resolve(__dirname, './fixture/config/options-with-config'); 75 | process.chdir(TEST_PATH); 76 | const result = config({ dir: 'aaa', output: 'bbb', theme: 'new' }); 77 | t.equal(result.dir, 'aaa'); 78 | t.equal(result.output, 'bbb'); 79 | t.equal(result.theme, 'new'); 80 | t.equal(result.customization, undefined); 81 | const doc = yaml.load(fs.readFileSync(path.join(TEST_PATH, 'loppo.yml'), 'utf8')); 82 | t.equal(doc.dir, 'ccc'); 83 | t.equal(doc.output, 'ddd'); 84 | t.end(); 85 | } 86 | ); 87 | -------------------------------------------------------------------------------- /test/copy.test.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const path = require('path'); 4 | const test = require('tape'); 5 | const fs = require('fs-extra'); 6 | const copy = require('../lib/copy'); 7 | 8 | const log = require('../lib/utils').log; 9 | log.setLevel('error'); 10 | 11 | const test_title = '[copy.js] '; 12 | 13 | test( 14 | test_title + 15 | 'copy theme directory to output directory', 16 | function (t) { 17 | const TEST_PATH = path.resolve(__dirname, './fixture/copy/copy-to-output-directory'); 18 | process.chdir(TEST_PATH); 19 | copy({ theme: 'oceandeep', dir: 'docs', output: 'dist' }); 20 | 21 | t.equal(fs.existsSync(path.resolve(process.cwd(), 'dist')), true); 22 | t.equal(fs.existsSync(path.resolve(process.cwd(), 'dist', 'index.html')), true); 23 | fs.removeSync(path.resolve(process.cwd(), 'dist')); 24 | t.end(); 25 | } 26 | ); 27 | 28 | test( 29 | test_title + 30 | 'customization is true', 31 | function (t) { 32 | const TEST_PATH = path.resolve(__dirname, './fixture/copy/customization-true'); 33 | process.chdir(TEST_PATH); 34 | copy({ 35 | customization: true, themeDir: 'loppo-theme', theme: 'oceandeep', dir: 'docs', output: 'dist' 36 | }); 37 | t.equal(fs.existsSync(path.resolve(process.cwd(), 'dist')), true); 38 | t.equal(fs.existsSync(path.resolve(process.cwd(), 'dist', 'assets', 'js', 'app.js')), true); 39 | fs.removeSync(path.resolve(process.cwd(), 'dist')); 40 | t.end(); 41 | } 42 | ); 43 | 44 | test( 45 | test_title + 46 | 'customization is false', 47 | function (t) { 48 | const TEST_PATH = path.resolve(__dirname, './fixture/copy/customization-false'); 49 | process.chdir(TEST_PATH); 50 | copy({ 51 | customization: false, themeDir: 'loppo-theme', theme: 'oceandeep', dir: 'docs', output: 'dist' 52 | }); 53 | t.equal(fs.existsSync(path.resolve(process.cwd(), 'dist')), true); 54 | t.equal(fs.existsSync(path.resolve(process.cwd(), 'dist', 'assets', 'css', 'app.css')), true); 55 | fs.removeSync(path.resolve(process.cwd(), 'dist')); 56 | t.end(); 57 | } 58 | ); 59 | 60 | test( 61 | test_title + 62 | 'excludes .template files', 63 | function (t) { 64 | const TEST_PATH = path.resolve(__dirname, './fixture/copy/excludes-template-files'); 65 | process.chdir(TEST_PATH); 66 | copy({ theme: 'oceandeep', dir: 'docs', output: 'dist' }); 67 | 68 | t.equal(fs.existsSync(path.resolve(process.cwd(), 'dist')), true); 69 | t.equal(fs.existsSync(path.resolve(process.cwd(), 'dist', 'index.template')), false); 70 | fs.removeSync(path.resolve(process.cwd(), 'dist')); 71 | t.end(); 72 | } 73 | ); 74 | -------------------------------------------------------------------------------- /test/fixture/chapters/chapters-file-already-exists/chapters.yml: -------------------------------------------------------------------------------- 1 | - a.md: a 2 | - b.md: b 3 | -------------------------------------------------------------------------------- /test/fixture/chapters/complex-docs-directory-structure/docs/a.md: -------------------------------------------------------------------------------- 1 | # Title A 2 | -------------------------------------------------------------------------------- /test/fixture/chapters/complex-docs-directory-structure/docs/b/c/d.md: -------------------------------------------------------------------------------- 1 | # Title D 2 | -------------------------------------------------------------------------------- /test/fixture/chapters/empty-chapters-file/chapters.yml: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ruanyf/loppo/962f08b3527ebb87d23611e2c2df4bea46d95502/test/fixture/chapters/empty-chapters-file/chapters.yml -------------------------------------------------------------------------------- /test/fixture/chapters/exclude-complex-empty-directory/docs/a.md: -------------------------------------------------------------------------------- 1 | # A 2 | -------------------------------------------------------------------------------- /test/fixture/chapters/exclude-complex-empty-directory/docs/images/dir3/b.md: -------------------------------------------------------------------------------- 1 | # B 2 | -------------------------------------------------------------------------------- /test/fixture/chapters/exclude-double-empty-directory/docs/a.md: -------------------------------------------------------------------------------- 1 | # A 2 | -------------------------------------------------------------------------------- /test/fixture/chapters/exclude-empty-directory/docs/a.md: -------------------------------------------------------------------------------- 1 | # A 2 | -------------------------------------------------------------------------------- /test/fixture/chapters/generating-chapters-file-complex-format/docs/a.md: -------------------------------------------------------------------------------- 1 | 2 | 3 | # Title A 4 | -------------------------------------------------------------------------------- /test/fixture/chapters/generating-chapters-file-complex-format/docs/b.md: -------------------------------------------------------------------------------- 1 | # Title B 2 | -------------------------------------------------------------------------------- /test/fixture/chapters/generating-chapters-file-filter/docs/_a.txt: -------------------------------------------------------------------------------- 1 | # Title B 2 | -------------------------------------------------------------------------------- /test/fixture/chapters/generating-chapters-file-filter/docs/a.txt: -------------------------------------------------------------------------------- 1 | # Title A 2 | -------------------------------------------------------------------------------- /test/fixture/chapters/no-docs-directory-exists/readme.md: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ruanyf/loppo/962f08b3527ebb87d23611e2c2df4bea46d95502/test/fixture/chapters/no-docs-directory-exists/readme.md -------------------------------------------------------------------------------- /test/fixture/chapters/simple-generating-chapters-file/docs/a.md: -------------------------------------------------------------------------------- 1 | # Title A 2 | -------------------------------------------------------------------------------- /test/fixture/chapters/simple-generating-chapters-file/docs/b.md: -------------------------------------------------------------------------------- 1 | # Title B 2 | -------------------------------------------------------------------------------- /test/fixture/config/no-options-no-default/.gitkeep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ruanyf/loppo/962f08b3527ebb87d23611e2c2df4bea46d95502/test/fixture/config/no-options-no-default/.gitkeep -------------------------------------------------------------------------------- /test/fixture/config/no-options-with-config/loppo.yml: -------------------------------------------------------------------------------- 1 | dir: aaa 2 | output: bbb 3 | theme: new 4 | -------------------------------------------------------------------------------- /test/fixture/config/options-no-config/.gitkeep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ruanyf/loppo/962f08b3527ebb87d23611e2c2df4bea46d95502/test/fixture/config/options-no-config/.gitkeep -------------------------------------------------------------------------------- /test/fixture/config/options-with-config/loppo.yml: -------------------------------------------------------------------------------- 1 | dir: ccc 2 | output: ddd 3 | -------------------------------------------------------------------------------- /test/fixture/copy/copy-to-output-directory/themes/oceandeep/index.html: -------------------------------------------------------------------------------- 1 | 2 | -------------------------------------------------------------------------------- /test/fixture/copy/customization-false/.gitkeep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ruanyf/loppo/962f08b3527ebb87d23611e2c2df4bea46d95502/test/fixture/copy/customization-false/.gitkeep -------------------------------------------------------------------------------- /test/fixture/copy/customization-true/loppo-theme/assets/js/app.js: -------------------------------------------------------------------------------- 1 | console.log('hello'); 2 | -------------------------------------------------------------------------------- /test/fixture/copy/excludes-template-files/themes/oceandeep/index.template: -------------------------------------------------------------------------------- 1 | 2 | -------------------------------------------------------------------------------- /test/fixture/makeContent/non-root-index-with-md-files/docs/dir1/a.md: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ruanyf/loppo/962f08b3527ebb87d23611e2c2df4bea46d95502/test/fixture/makeContent/non-root-index-with-md-files/docs/dir1/a.md -------------------------------------------------------------------------------- /test/fixture/makeContent/non-root-index-with-md-files/docs/dir1/b.md: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ruanyf/loppo/962f08b3527ebb87d23611e2c2df4bea46d95502/test/fixture/makeContent/non-root-index-with-md-files/docs/dir1/b.md -------------------------------------------------------------------------------- /test/fixture/makeContent/regular-markdown-file/docs/first.md: -------------------------------------------------------------------------------- 1 | This is a regular file. 2 | -------------------------------------------------------------------------------- /test/fixture/makeContent/root-index-with-readme/README.md: -------------------------------------------------------------------------------- 1 | This is a README file. 2 | -------------------------------------------------------------------------------- /test/fixture/makeContent/root-index-without-readme/.gitkeep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ruanyf/loppo/962f08b3527ebb87d23611e2c2df4bea46d95502/test/fixture/makeContent/root-index-without-readme/.gitkeep -------------------------------------------------------------------------------- /test/fixture/theme/customization-true-no-themedir-exists/.gitkeep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ruanyf/loppo/962f08b3527ebb87d23611e2c2df4bea46d95502/test/fixture/theme/customization-true-no-themedir-exists/.gitkeep -------------------------------------------------------------------------------- /test/fixture/theme/customization-true-old-themedir-exists/themes/oceandeep/page.template: -------------------------------------------------------------------------------- 1 | hello world 2 | -------------------------------------------------------------------------------- /test/fixture/theme/customization-true-themedir-exists/loppo-theme/page.template: -------------------------------------------------------------------------------- 1 | hello world 2 | -------------------------------------------------------------------------------- /test/fixture/theme/includes/themes/oceandeep/author.template: -------------------------------------------------------------------------------- 1 | World 2 | -------------------------------------------------------------------------------- /test/fixture/theme/includes/themes/oceandeep/date.template: -------------------------------------------------------------------------------- 1 | 2016 2 | -------------------------------------------------------------------------------- /test/fixture/theme/includes/themes/oceandeep/page.template: -------------------------------------------------------------------------------- 1 | <% includes title %> <% includes author %> 2 | new 3 | <% includes date %> 4 | -------------------------------------------------------------------------------- /test/fixture/theme/includes/themes/oceandeep/title.template: -------------------------------------------------------------------------------- 1 |

Hello

2 | -------------------------------------------------------------------------------- /test/fixture/theme/theme-exists/themes/oceandeep/page.template: -------------------------------------------------------------------------------- 1 | hello world 2 | -------------------------------------------------------------------------------- /test/fixture/theme/theme-not-exists/themes/.gitkeep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ruanyf/loppo/962f08b3527ebb87d23611e2c2df4bea46d95502/test/fixture/theme/theme-not-exists/themes/.gitkeep -------------------------------------------------------------------------------- /test/fixture/writePage/simple-write/.gitkeep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ruanyf/loppo/962f08b3527ebb87d23611e2c2df4bea46d95502/test/fixture/writePage/simple-write/.gitkeep -------------------------------------------------------------------------------- /test/makeBreadcrumb.test.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | // const path = require('path'); 4 | const test = require('tape'); 5 | // const fs = require('fs-extra'); 6 | const makeBreadcrumbOrigin = require('../lib/utils').makeBreadcrumbOrigin; 7 | 8 | const log = require('../lib/utils').log; 9 | log.setLevel('error'); 10 | 11 | const test_title = '[utils.js/makeBreadcrumbOrigin] '; 12 | 13 | test( 14 | test_title + 15 | 'root directory', 16 | function (t) { 17 | const opt = { 18 | dir: 'docs', 19 | site: 'My Site', 20 | chapters: [ 21 | { 'a.md': 'Title A' }, 22 | { 'dir1/': 'dir1' }, 23 | { 'dir1/dir2/': 'dir2' }, 24 | { 'dir1/dir2/b.md': 'Title B' } 25 | ] 26 | }; 27 | const optionObj = makeBreadcrumbOrigin('/', opt); 28 | t.deepEqual(optionObj.breadcrumbOrigin, [{ path: 'index.html', text: 'Home' }]); 29 | t.end(); 30 | } 31 | ); 32 | 33 | test( 34 | test_title + 35 | 'index.md', 36 | function (t) { 37 | const opt = { 38 | dir: 'docs', 39 | site: 'My Site', 40 | chapters: [ 41 | { 'index.md': 'Index' }, 42 | { 'a.md': 'Title A' }, 43 | { 'dir1/': 'dir1' }, 44 | { 'dir1/dir2/': 'dir2' }, 45 | { 'dir1/dir2/b.md': 'Title B' } 46 | ] 47 | }; 48 | const optionObj = makeBreadcrumbOrigin('index.md', opt); 49 | t.deepEqual(optionObj.breadcrumbOrigin, [{ path: 'index.html', text: 'Index' }]); 50 | t.end(); 51 | } 52 | ); 53 | 54 | test( 55 | test_title + 56 | 'top level path', 57 | function (t) { 58 | const opt = { 59 | dir: 'docs', 60 | site: 'My Site', 61 | chapters: [ 62 | { 'index.md': 'Index' }, 63 | { 'a.md': 'Title A' }, 64 | { 'dir1/': 'dir1' }, 65 | { 'dir1/dir2/': 'dir2' }, 66 | { 'dir1/dir2/b.md': 'Title B' } 67 | ] 68 | }; 69 | const optionObj = makeBreadcrumbOrigin('a.md', opt); 70 | t.deepEqual(optionObj.breadcrumbOrigin, [{ path: 'index.html', text: 'Home' }, { path: 'a.html', text: 'Title A' }]); 71 | t.end(); 72 | } 73 | ); 74 | 75 | test( 76 | test_title + 77 | 'sub-directory', 78 | function (t) { 79 | const opt = { 80 | dir: 'docs', 81 | site: 'My Site', 82 | chapters: [ 83 | { 'index.md': 'Index' }, 84 | { 'a.md': 'Title A' }, 85 | { 'dir1/': 'dir1' }, 86 | { 'dir1/dir2/': 'dir2' }, 87 | { 'dir1/dir2/b.md': 'Title B' } 88 | ] 89 | }; 90 | const optionObj = makeBreadcrumbOrigin('dir1/', opt); 91 | t.deepEqual(optionObj.breadcrumbOrigin, [{ path: 'index.html', text: 'Home' }, { path: 'dir1/index.html', text: 'dir1' }]); 92 | t.end(); 93 | } 94 | ); 95 | 96 | test( 97 | test_title + 98 | 'sub-directory file', 99 | function (t) { 100 | const opt = { 101 | dir: 'docs', 102 | site: 'My Site', 103 | chapters: [ 104 | { 'index.md': 'Index' }, 105 | { 'a.md': 'Title A' }, 106 | { 'dir1/': 'dir1' }, 107 | { 'dir1/dir2/': 'dir2' }, 108 | { 'dir1/dir2/b.md': 'Title B' } 109 | ] 110 | }; 111 | const optionObj = makeBreadcrumbOrigin('dir1/dir2/b.md', opt); 112 | t.deepEqual(optionObj.breadcrumbOrigin, [ 113 | { path: 'index.html', text: 'Home' }, 114 | { path: 'dir1/index.html', text: 'dir1' }, 115 | { path: 'dir1/dir2/index.html', text: 'dir2' }, 116 | { path: 'dir1/dir2/b.html', text: 'Title B' } 117 | ]); 118 | t.end(); 119 | } 120 | ); 121 | 122 | test( 123 | test_title + 124 | 'sub-directory index.md', 125 | function (t) { 126 | const opt = { 127 | dir: 'docs', 128 | site: 'My Site', 129 | chapters: [ 130 | { 'index.md': 'Index' }, 131 | { 'a.md': 'Title A' }, 132 | { 'dir1/': 'dir1' }, 133 | { 'dir1/dir2/': 'dir2' }, 134 | { 'dir1/dir2/index.md': 'Sub Index' } 135 | ] 136 | }; 137 | const optionObj = makeBreadcrumbOrigin('dir1/dir2/index.md', opt); 138 | t.deepEqual(optionObj.breadcrumbOrigin, [ 139 | { path: 'index.html', text: 'Home' }, 140 | { path: 'dir1/index.html', text: 'dir1' }, 141 | { path: 'dir1/dir2/index.html', text: 'Sub Index' } 142 | ]); 143 | t.end(); 144 | } 145 | ); 146 | -------------------------------------------------------------------------------- /test/makeBuildTime.test.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | // const path = require('path'); 4 | const test = require('tape'); 5 | // const fs = require('fs-extra'); 6 | const makeBuildTime = require('../lib/utils').makeBuildTime; 7 | 8 | const log = require('../lib/utils').log; 9 | log.setLevel('error'); 10 | 11 | const test_title = '[utils.js/makeBuildTime] '; 12 | 13 | test( 14 | test_title + 15 | 'should be a Date instance', 16 | function (t) { 17 | const opt = { 18 | dir: 'docs', 19 | site: 'My Site', 20 | chapters: [] 21 | }; 22 | const optionObj = makeBuildTime('/', opt); 23 | t.ok(optionObj.build_time instanceof Date); 24 | t.end(); 25 | } 26 | ); 27 | -------------------------------------------------------------------------------- /test/makeContent.test.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const path = require('path'); 4 | const test = require('tape'); 5 | const makeContent = require('../lib/utils').makeContent; 6 | 7 | const log = require('../lib/utils').log; 8 | log.setLevel('error'); 9 | 10 | const test_title = '[utils.js/makeContent] '; 11 | 12 | test( 13 | test_title + 14 | 'regular markdown file', 15 | function (t) { 16 | const TEST_PATH = path.resolve(__dirname, './fixture/makeContent/regular-markdown-file'); 17 | process.chdir(TEST_PATH); 18 | let opt = { 19 | dir: 'docs', 20 | chapters: [ 21 | { 'first.md': 'First' }, 22 | { 'dir1/': 'dir1' }, 23 | { 'dir1/a.md': 'Title A' }, 24 | { 'dir2/': 'dir2' }, 25 | { 'dir2/b.md': 'Title B' } 26 | ] 27 | }; 28 | opt = makeContent('first.md', opt); 29 | let result = ''; 30 | if (path.sep !== '/') { 31 | result = 'This is a regular file.\r\n'; 32 | } else { 33 | result = 'This is a regular file.\n'; 34 | } 35 | t.equal(opt.content, result); 36 | t.end(); 37 | } 38 | ); 39 | 40 | test( 41 | test_title + 42 | 'root index with README', 43 | function (t) { 44 | const TEST_PATH = path.resolve(__dirname, './fixture/makeContent/root-index-with-readme'); 45 | process.chdir(TEST_PATH); 46 | let opt = { 47 | dir: 'docs', 48 | chapters: [ 49 | { 'dir1/': 'dir1' }, 50 | { 'dir1/a.md': 'Title A' }, 51 | { 'dir2/': 'dir2' }, 52 | { 'dir2/b.md': 'Title B' } 53 | ] 54 | }; 55 | opt = makeContent('/', opt); 56 | let result = ''; 57 | if (path.sep !== '/') { 58 | result = 'This is a README file.\r\n'; 59 | } else { 60 | result = 'This is a README file.\n'; 61 | } 62 | t.equal(opt.content, result); 63 | t.end(); 64 | } 65 | ); 66 | 67 | test( 68 | test_title + 69 | 'root index without README', 70 | function (t) { 71 | const TEST_PATH = path.resolve(__dirname, './fixture/makeContent/root-index-without-readme'); 72 | process.chdir(TEST_PATH); 73 | let opt = { 74 | dir: 'docs', 75 | chapters: [ 76 | { 'dir1/': 'dir1' }, 77 | { 'dir1/a.md': 'Title A' }, 78 | { 'dir2/': 'dir2' }, 79 | { 'dir2/b.md': 'Title B' } 80 | ] 81 | }; 82 | opt = makeContent('/', opt); 83 | t.equal(opt.content, ''); 84 | t.end(); 85 | } 86 | ); 87 | 88 | test( 89 | test_title + 90 | 'non root index with md files', 91 | function (t) { 92 | const TEST_PATH = path.resolve(__dirname, './fixture/makeContent/non-root-index-with-md-files'); 93 | process.chdir(TEST_PATH); 94 | let opt = { 95 | dir: 'docs', 96 | chapters: [ 97 | { 'dir1/': 'dir1' }, 98 | { 'dir1/a.md': 'Title A' }, 99 | { 'dir1/b.md': 'Title B' } 100 | ] 101 | }; 102 | opt = makeContent('dir1/', opt); 103 | t.equal(opt.content, '- [Title A](a.html)\n- [Title B](b.html)\n'); 104 | t.end(); 105 | } 106 | ); 107 | 108 | test( 109 | test_title + 110 | 'non root index with md files and sub-directories', 111 | function (t) { 112 | let opt = { 113 | dir: 'docs', 114 | chapters: [ 115 | { 'dir1/': 'dir1' }, 116 | { 'dir1/a.md': 'Title A' }, 117 | { 'dir1/b.md': 'Title B' }, 118 | { 'dir1/dir2/': 'dir2' }, 119 | { 'dir1/dir2/c.md': 'Title C' }, 120 | { 'dir1/dir2/dir3/': 'dir3' }, 121 | { 'dir1/dir2/dir3/d.md': 'Title D' } 122 | ] 123 | }; 124 | opt = makeContent('dir1/', opt); 125 | t.equal(opt.content, '- [Title A](a.html)\n- [Title B](b.html)\n- [dir2](dir2/)\n'); 126 | t.end(); 127 | } 128 | ); 129 | -------------------------------------------------------------------------------- /test/makeIndex.test.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | // const path = require('path'); 4 | const test = require('tape'); 5 | // const fs = require('fs-extra'); 6 | const makeIndex = require('../lib/utils').makeIndex; 7 | 8 | const log = require('../lib/utils').log; 9 | log.setLevel('error'); 10 | 11 | const test_title = '[utils.js/makeIndex] '; 12 | 13 | test( 14 | test_title + 15 | 'root param: Not root index, and not in Chapters Array, should throw an error', 16 | function (t) { 17 | const opt = { 18 | chapters: ['dir1/', 'dir1/a.md', 'dir2/', 'dir2/b.md'] 19 | }; 20 | t.throws(function () { 21 | makeIndex('dir3/', opt); 22 | }); 23 | t.end(); 24 | } 25 | ); 26 | 27 | -------------------------------------------------------------------------------- /test/makeNextPageObject.test.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | // const path = require('path'); 4 | const test = require('tape'); 5 | // const fs = require('fs-extra'); 6 | const makeNextPageObject = require('../lib/utils').makeNextPageObject; 7 | 8 | const log = require('../lib/utils').log; 9 | log.setLevel('error'); 10 | 11 | const test_title = '[utils.js/makeNextPageObject] '; 12 | 13 | test( 14 | test_title + 15 | 'root directory and no other content', 16 | function (t) { 17 | const opt = { 18 | dir: 'docs', 19 | site: 'My Site', 20 | chapters: [] 21 | }; 22 | const optionObj = makeNextPageObject('/', opt); 23 | t.equal(optionObj.next_page_object, null); 24 | t.end(); 25 | } 26 | ); 27 | 28 | test( 29 | test_title + 30 | 'root directory and the only item is index.md', 31 | function (t) { 32 | const opt = { 33 | dir: 'docs', 34 | site: 'My Site', 35 | chapters: [ 36 | { 'index.md': 'Title Index' } 37 | ] 38 | }; 39 | const optionObj = makeNextPageObject('/', opt); 40 | t.equal(optionObj.next_page_object, null); 41 | t.end(); 42 | } 43 | ); 44 | 45 | test( 46 | test_title + 47 | 'root directory and chapters.yml\'s first item is not index.md', 48 | function (t) { 49 | const opt = { 50 | dir: 'docs', 51 | site: 'My Site', 52 | chapters: [ 53 | { 'a.md': 'Title A' } 54 | ] 55 | }; 56 | const optionObj = makeNextPageObject('/', opt); 57 | t.deepEqual(optionObj.next_page_object, { 'a.md': 'Title A' }); 58 | t.end(); 59 | } 60 | ); 61 | 62 | test( 63 | test_title + 64 | 'index.md and it is the only item', 65 | function (t) { 66 | const opt = { 67 | dir: 'docs', 68 | site: 'My Site', 69 | chapters: [ 70 | { 'index.md': 'Title Index' } 71 | ] 72 | }; 73 | const optionObj = makeNextPageObject('index.md', opt); 74 | t.equal(optionObj.next_page_object, null); 75 | t.end(); 76 | } 77 | ); 78 | 79 | test( 80 | test_title + 81 | 'index.md and it is not the only item', 82 | function (t) { 83 | const opt = { 84 | dir: 'docs', 85 | site: 'My Site', 86 | chapters: [ 87 | { 'index.md': 'Title Index' }, 88 | { 'a.md': 'Title A' } 89 | ] 90 | }; 91 | const optionObj = makeNextPageObject('index.md', opt); 92 | t.deepEqual(optionObj.next_page_object, { 'a.md': 'Title A' }); 93 | t.end(); 94 | } 95 | ); 96 | 97 | test( 98 | test_title + 99 | 'regular item and it is the last item', 100 | function (t) { 101 | const opt = { 102 | dir: 'docs', 103 | site: 'My Site', 104 | chapters: [ 105 | { 'a.md': 'Title A' }, 106 | { 'b.md': 'Title B' } 107 | ] 108 | }; 109 | const optionObj = makeNextPageObject('a.md', opt); 110 | t.deepEqual(optionObj.next_page_object, { 'b.md': 'Title B' }); 111 | t.end(); 112 | } 113 | ); 114 | 115 | test( 116 | test_title + 117 | 'regular item and it is not the last item', 118 | function (t) { 119 | const opt = { 120 | dir: 'docs', 121 | site: 'My Site', 122 | chapters: [ 123 | { 'a.md': 'Title A' }, 124 | { 'b.md': 'Title B' } 125 | ] 126 | }; 127 | const optionObj = makeNextPageObject('b.md', opt); 128 | t.deepEqual(optionObj.next_page_object, null); 129 | t.end(); 130 | } 131 | ); 132 | 133 | test( 134 | test_title + 135 | 'sub level index.md', 136 | function (t) { 137 | const opt = { 138 | dir: 'docs', 139 | site: 'My Site', 140 | chapters: [ 141 | { 'a.md': 'Title A' }, 142 | { 'dir1/': 'dir1' }, 143 | { 'dir1/index.md': 'Sub Index' }, 144 | { 'dir1/b.md': 'Title B' } 145 | ] 146 | }; 147 | const optionObj = makeNextPageObject('dir1/', opt); 148 | t.deepEqual(optionObj.next_page_object, { 'dir1/b.md': 'Title B' }); 149 | t.end(); 150 | } 151 | ); 152 | -------------------------------------------------------------------------------- /test/makePageTitle.test.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | // const path = require('path'); 4 | const test = require('tape'); 5 | // const fs = require('fs-extra'); 6 | const makePageTitle = require('../lib/utils').makePageTitle; 7 | 8 | const log = require('../lib/utils').log; 9 | log.setLevel('error'); 10 | 11 | const test_title = '[utils.js/makePageTitle] '; 12 | 13 | test( 14 | test_title + 15 | 'regular file\'s content has page title', 16 | function (t) { 17 | const opt = { 18 | dir: 'docs', 19 | site: 'My Site', 20 | chapters: [ 21 | { 'dir1/': 'dir1' }, 22 | { 'dir1/a.md': 'Title A' }, 23 | { 'dir2/': 'dir2' }, 24 | { 'dir2/b.md': 'Title B' } 25 | ], 26 | content: '# My Title\n\nThis is an example.\n' 27 | }; 28 | const optionObj = makePageTitle('dir1/a.md', opt); 29 | t.equal(optionObj.page_title, 'My Title'); 30 | t.equal(optionObj.content, '\nThis is an example.\n'); 31 | t.end(); 32 | } 33 | ); 34 | 35 | test( 36 | test_title + 37 | 'regular file\'s content has no page title', 38 | function (t) { 39 | const opt = { 40 | dir: 'docs', 41 | site: 'My Site', 42 | chapters: [ 43 | { 'dir1/': 'dir1' }, 44 | { 'dir1/a.md': 'Title A' }, 45 | { 'dir2/': 'dir2' }, 46 | { 'dir2/b.md': 'Title B' } 47 | ], 48 | content: 'This is an example.\n' 49 | }; 50 | const optionObj = makePageTitle('dir1/a.md', opt); 51 | t.equal(optionObj.page_title, 'Title A'); 52 | t.equal(optionObj.content, 'This is an example.\n'); 53 | t.end(); 54 | } 55 | ); 56 | 57 | test( 58 | test_title + 59 | 'regular file content has no content', 60 | function (t) { 61 | const opt = { 62 | dir: 'docs', 63 | site: 'My Site', 64 | chapters: [ 65 | { 'dir1/': 'dir1' }, 66 | { 'dir1/a.md': 'Title A' }, 67 | { 'dir2/': 'dir2' }, 68 | { 'dir2/b.md': 'Title B' } 69 | ], 70 | content: '' 71 | }; 72 | const optionObj = makePageTitle('dir1/a.md', opt); 73 | t.equal(optionObj.page_title, 'Title A'); 74 | t.end(); 75 | } 76 | ); 77 | 78 | test( 79 | test_title + 80 | 'sub-directory', 81 | function (t) { 82 | const opt = { 83 | dir: 'docs', 84 | site: 'My Site', 85 | chapters: [ 86 | { 'dir1/': 'dir1' }, 87 | { 'dir1/a.md': 'Title A' }, 88 | { 'dir2/': 'dir2' }, 89 | { 'dir2/b.md': 'Title B' } 90 | ], 91 | content: '' 92 | }; 93 | const optionObj = makePageTitle('dir1/', opt); 94 | t.equal(optionObj.page_title, 'dir1'); 95 | t.end(); 96 | } 97 | ); 98 | 99 | test( 100 | test_title + 101 | 'root directory has no content', 102 | function (t) { 103 | const opt = { 104 | dir: 'docs', 105 | site: 'My Site', 106 | chapters: [ 107 | { 'dir1/': 'dir1' }, 108 | { 'dir1/a.md': 'Title A' }, 109 | { 'dir2/': 'dir2' }, 110 | { 'dir2/b.md': 'Title B' } 111 | ], 112 | content: '' 113 | }; 114 | const optionObj = makePageTitle('/', opt); 115 | t.equal(optionObj.page_title, 'My Site'); 116 | t.end(); 117 | } 118 | ); 119 | 120 | test( 121 | test_title + 122 | 'root directory has content and page title', 123 | function (t) { 124 | const opt = { 125 | dir: 'docs', 126 | site: 'My Site', 127 | chapters: [ 128 | { 'dir1/': 'dir1' }, 129 | { 'dir1/a.md': 'Title A' }, 130 | { 'dir2/': 'dir2' }, 131 | { 'dir2/b.md': 'Title B' } 132 | ], 133 | content: '# Readme\n\nThis is an example.\n' 134 | }; 135 | const optionObj = makePageTitle('/', opt); 136 | t.equal(optionObj.page_title, 'Readme'); 137 | t.equal(optionObj.content, '\nThis is an example.\n'); 138 | t.end(); 139 | } 140 | ); 141 | 142 | test( 143 | test_title + 144 | 'root directory has content and no page title', 145 | function (t) { 146 | const opt = { 147 | dir: 'docs', 148 | site: 'My Site', 149 | chapters: [ 150 | { 'dir1/': 'dir1' }, 151 | { 'dir1/a.md': 'Title A' }, 152 | { 'dir2/': 'dir2' }, 153 | { 'dir2/b.md': 'Title B' } 154 | ], 155 | content: 'This is an example.\n' 156 | }; 157 | const optionObj = makePageTitle('/', opt); 158 | t.equal(optionObj.page_title, 'My Site'); 159 | t.equal(optionObj.content, 'This is an example.\n'); 160 | t.end(); 161 | } 162 | ); 163 | -------------------------------------------------------------------------------- /test/makePreviousPageObject.test.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | // const path = require('path'); 4 | const test = require('tape'); 5 | // const fs = require('fs-extra'); 6 | const makePreviousPageObject = require('../lib/utils').makePreviousPageObject; 7 | 8 | const log = require('../lib/utils').log; 9 | log.setLevel('error'); 10 | 11 | const test_title = '[utils.js/makePreviousPageObject] '; 12 | 13 | test( 14 | test_title + 15 | 'root directory', 16 | function (t) { 17 | const opt = { 18 | dir: 'docs', 19 | site: 'My Site', 20 | chapters: [ 21 | { 'dir1/': 'dir1' }, 22 | { 'dir1/a.md': 'Title A' }, 23 | { 'dir2/': 'dir2' }, 24 | { 'dir2/b.md': 'Title B' } 25 | ] 26 | }; 27 | const optionObj = makePreviousPageObject('/', opt); 28 | t.equal(optionObj.previous_page_object, null); 29 | t.end(); 30 | } 31 | ); 32 | 33 | test( 34 | test_title + 35 | 'first item is index.md', 36 | function (t) { 37 | const opt = { 38 | dir: 'docs', 39 | site: 'My Site', 40 | chapters: [ 41 | { 'index.md': 'Home' }, 42 | { 'dir1/': 'dir1' }, 43 | { 'dir1/a.md': 'Title A' }, 44 | { 'dir2/': 'dir2' }, 45 | { 'dir2/b.md': 'Title B' } 46 | ] 47 | }; 48 | const optionObj = makePreviousPageObject('index.md', opt); 49 | t.equal(optionObj.previous_page_object, null); 50 | t.end(); 51 | } 52 | ); 53 | 54 | test( 55 | test_title + 56 | 'first item is not index.md', 57 | function (t) { 58 | const opt = { 59 | dir: 'docs', 60 | site: 'My Site', 61 | chapters: [ 62 | { 'x.md': 'Title X' }, 63 | { 'dir1/': 'dir1' }, 64 | { 'dir1/a.md': 'Title A' }, 65 | { 'dir2/': 'dir2' }, 66 | { 'dir2/b.md': 'Title B' } 67 | ] 68 | }; 69 | const optionObj = makePreviousPageObject('x.md', opt); 70 | t.deepEqual(optionObj.previous_page_object, { 'index.md': 'Home' }); 71 | t.end(); 72 | } 73 | ); 74 | 75 | test( 76 | test_title + 77 | 'not first item', 78 | function (t) { 79 | const opt = { 80 | dir: 'docs', 81 | site: 'My Site', 82 | chapters: [ 83 | { 'x.md': 'Title X' }, 84 | { 'dir1/': 'dir1' }, 85 | { 'dir1/a.md': 'Title A' }, 86 | { 'dir2/': 'dir2' }, 87 | { 'dir2/b.md': 'Title B' } 88 | ] 89 | }; 90 | const optionObj = makePreviousPageObject('dir1/a.md', opt); 91 | t.deepEqual(optionObj.previous_page_object, { 'dir1/': 'dir1' }); 92 | t.end(); 93 | } 94 | ); 95 | 96 | test( 97 | test_title + 98 | 'sub level index.md', 99 | function (t) { 100 | const opt = { 101 | dir: 'docs', 102 | site: 'My Site', 103 | chapters: [ 104 | { 'x.md': 'Title X' }, 105 | { 'dir1/': 'dir1' }, 106 | { 'dir1/index.md': 'Sub Index' }, 107 | { 'dir1/a.md': 'Title A' }, 108 | { 'dir2/': 'dir2' }, 109 | { 'dir2/b.md': 'Title B' } 110 | ] 111 | }; 112 | const optionObj = makePreviousPageObject('dir1/index.md', opt); 113 | t.deepEqual(optionObj.previous_page_object, { 'x.md': 'Title X' }); 114 | t.end(); 115 | } 116 | ); 117 | -------------------------------------------------------------------------------- /test/makeRelativeRootPath.test.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | // const path = require('path'); 4 | const test = require('tape'); 5 | // const fs = require('fs-extra'); 6 | const makeRelativeRootPath = require('../lib/utils').makeRelativeRootPath; 7 | 8 | const log = require('../lib/utils').log; 9 | log.setLevel('error'); 10 | 11 | const test_title = '[utils.js/makeRootRelativePath] '; 12 | 13 | test( 14 | test_title + 15 | 'root directory', 16 | function (t) { 17 | const opt = { 18 | dir: 'docs', 19 | site: 'My Site', 20 | chapters: [] 21 | }; 22 | const optionObj = makeRelativeRootPath('/', opt); 23 | t.equal(optionObj.relative_root_path, './'); 24 | t.end(); 25 | } 26 | ); 27 | 28 | test( 29 | test_title + 30 | 'top level path', 31 | function (t) { 32 | const opt = { 33 | dir: 'docs', 34 | site: 'My Site', 35 | chapters: [ 36 | { 'a.md': 'Title A' } 37 | ] 38 | }; 39 | const optionObj = makeRelativeRootPath('a.md', opt); 40 | t.equal(optionObj.relative_root_path, ''); 41 | t.end(); 42 | } 43 | ); 44 | 45 | test( 46 | test_title + 47 | 'sub level path', 48 | function (t) { 49 | const opt = { 50 | dir: 'docs', 51 | site: 'My Site', 52 | chapters: [ 53 | { 'a.md': 'Title A' }, 54 | { 'dir1/': 'dir1' }, 55 | { 'dir1/dir2/': 'dir2' }, 56 | { 'dir1/dir2/b.md': 'Title B' } 57 | ] 58 | }; 59 | const optionObj = makeRelativeRootPath('dir1/', opt); 60 | t.equal(optionObj.relative_root_path, '../'); 61 | t.end(); 62 | } 63 | ); 64 | 65 | test( 66 | test_title + 67 | 'sub level path', 68 | function (t) { 69 | const opt = { 70 | dir: 'docs', 71 | site: 'My Site', 72 | chapters: [ 73 | { 'a.md': 'Title A' }, 74 | { 'dir1/': 'dir1' }, 75 | { 'dir1/dir2/': 'dir2' }, 76 | { 'dir1/dir2/b.md': 'Title B' } 77 | ] 78 | }; 79 | const optionObj = makeRelativeRootPath('dir1/dir2/b.md', opt); 80 | t.equal(optionObj.relative_root_path, '../../'); 81 | t.end(); 82 | } 83 | ); 84 | 85 | -------------------------------------------------------------------------------- /test/markdownRender.test.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | // const path = require('path'); 4 | const test = require('tape'); 5 | // const fs = require('fs-extra'); 6 | const markdownRender = require('../lib/utils').markdownRender; 7 | 8 | const log = require('../lib/utils').log; 9 | log.setLevel('error'); 10 | 11 | const test_title = '[utils.js/markdownRender] '; 12 | 13 | test( 14 | test_title + 15 | 'regular file', 16 | function (t) { 17 | const opt = { 18 | dir: 'docs', 19 | site: 'My Site', 20 | chapters: [ 21 | { 'dir1/': 'dir1' }, 22 | { 'dir1/a.md': 'Title A' }, 23 | { 'dir2/': 'dir2' }, 24 | { 'dir2/b.md': 'Title B' } 25 | ], 26 | content: '# My Title\n\nThis is an example.\n' 27 | }; 28 | const optionObj = markdownRender('dir1/a.md', opt); 29 | t.equal(optionObj.toc, ''); 30 | t.equal(optionObj.content, '

My Title #

\n

This is an example.

\n'); 31 | t.end(); 32 | } 33 | ); 34 | 35 | test( 36 | test_title + 37 | 'special title', 38 | function (t) { 39 | const opt = { 40 | dir: 'docs', 41 | site: 'My Site', 42 | chapters: [ 43 | { 'dir1/': 'dir1' }, 44 | { 'dir1/a.md': 'Title A' }, 45 | { 'dir2/': 'dir2' }, 46 | { 'dir2/b.md': 'Title B' } 47 | ], 48 | content: '## HTML 文档的”标题“\n' 49 | }; 50 | const optionObj = markdownRender('dir1/a.md', opt); 51 | t.equal(optionObj.toc, '\n'); 52 | t.equal(optionObj.content, '

HTML 文档的”标题“ #

\n'); 53 | t.end(); 54 | } 55 | ); 56 | 57 | test( 58 | test_title + 59 | 'toc', 60 | function (t) { 61 | const opt = { 62 | dir: 'docs', 63 | site: 'My Site', 64 | chapters: [ 65 | { 'dir1/': 'dir1' }, 66 | { 'dir1/a.md': 'Title A' }, 67 | { 'dir2/': 'dir2' }, 68 | { 'dir2/b.md': 'Title B' } 69 | ], 70 | content: '# My Title\n\n## Title 1\n## Title 2\n### Title 3\n### Title 4\n' 71 | }; 72 | const optionObj = markdownRender('dir1/a.md', opt); 73 | t.equal(optionObj.toc, '\n'); 74 | t.equal(optionObj.content, '

My Title #

\n

Title 1 #

\n

Title 2 #

\n

Title 3 #

\n

Title 4 #

\n'); 75 | t.end(); 76 | } 77 | ); 78 | 79 | test( 80 | test_title + 81 | 'Chinese title', 82 | function (t) { 83 | const opt = { 84 | dir: 'docs', 85 | site: 'My Site', 86 | chapters: [ 87 | { 'dir1/': 'dir1' }, 88 | { 'dir1/a.md': 'Title A' }, 89 | { 'dir2/': 'dir2' }, 90 | { 'dir2/b.md': 'Title B' } 91 | ], 92 | content: '## 你好\n\n### 世界\n' 93 | }; 94 | const optionObj = markdownRender('dir1/a.md', opt); 95 | t.equal(optionObj.toc, '\n'); 96 | t.equal(optionObj.content, '

你好 #

\n

世界 #

\n'); 97 | t.end(); 98 | } 99 | ); 100 | 101 | test( 102 | test_title + 103 | 'code highlight', 104 | function (t) { 105 | const opt = { 106 | dir: 'docs', 107 | site: 'My Site', 108 | chapters: [ 109 | { 'dir1/': 'dir1' }, 110 | { 'dir1/a.md': 'Title A' }, 111 | { 'dir2/': 'dir2' }, 112 | { 'dir2/b.md': 'Title B' } 113 | ], 114 | content: '```javascript\nconsole.log("hello world");\n```\n' 115 | }; 116 | const optionObj = markdownRender('dir1/a.md', opt); 117 | t.equal(optionObj.toc, ''); 118 | t.equal(optionObj.content, '
console.log("hello world");\n
\n'); 119 | t.end(); 120 | } 121 | ); 122 | -------------------------------------------------------------------------------- /test/theme.test.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const path = require('path'); 4 | const test = require('tape'); 5 | const fs = require('fs-extra'); 6 | const theme = require('../lib/theme'); 7 | // const walkSync = require('walk-sync'); 8 | 9 | const log = require('../lib/utils').log; 10 | log.setLevel('error'); 11 | 12 | const test_title = '[theme.js] '; 13 | const THEME = 'oceandeep'; 14 | const THEMEDIR = 'loppo-theme'; 15 | 16 | test( 17 | test_title + 18 | 'if the theme directory already exists, the directory will have no changes', 19 | function (t) { 20 | const TEST_PATH = path.resolve(__dirname, './fixture/theme/theme-exists'); 21 | process.chdir(TEST_PATH); 22 | const option = theme({ theme: THEME }); 23 | // const result = walkSync(path.resolve(process.cwd(), 'themes', THEME)); 24 | let result = ''; 25 | if (path.sep !== '/') { 26 | result = 'hello world\r\n'; 27 | } else { 28 | result = 'hello world\n'; 29 | } 30 | t.equal(option.templates.page(), result); 31 | t.end(); 32 | } 33 | ); 34 | 35 | test( 36 | test_title + 37 | 'if the theme directory does not exist, the directory will be created', 38 | function (t) { 39 | const TEST_PATH = path.resolve(__dirname, './fixture/theme/theme-not-exists'); 40 | process.chdir(TEST_PATH); 41 | const option = theme({ theme: THEME }); 42 | // const result = walkSync(path.resolve(process.cwd(), 'themes', THEME)); 43 | 44 | t.equal(typeof option.templates.page, 'function'); 45 | t.end(); 46 | fs.removeSync(path.resolve(process.cwd(), 'themes', THEME)); 47 | } 48 | ); 49 | 50 | test( 51 | test_title + 52 | '<% includes %> syntax', 53 | function (t) { 54 | const TEST_PATH = path.resolve(__dirname, './fixture/theme/includes'); 55 | process.chdir(TEST_PATH); 56 | const option = theme({ theme: THEME }); 57 | // const result = walkSync(path.resolve(process.cwd(), 'themes', THEME)); 58 | let result = ''; 59 | if (path.sep !== '/') { 60 | result = '

Hello

\r\n World\r\n\r\nnew\r\n2016\r\n\r\n'; 61 | } else { 62 | result = '

Hello

\n World\n\nnew\n2016\n\n'; 63 | } 64 | t.equal(option.templates.page(), result); 65 | t.end(); 66 | // fs.removeSync(path.resolve(process.cwd(), 'themes', THEME)); 67 | } 68 | ); 69 | 70 | test( 71 | test_title + 72 | 'customization is true and theme dir is existed', 73 | function (t) { 74 | const TEST_PATH = path.resolve(__dirname, './fixture/theme/customization-true-themedir-exists'); 75 | process.chdir(TEST_PATH); 76 | const option = theme({ customization: true }); 77 | let result = ''; 78 | if (path.sep !== '/') { 79 | result = 'hello world\r\n'; 80 | } else { 81 | result = 'hello world\n'; 82 | } 83 | t.equal(option.templates.page(), result); 84 | t.end(); 85 | } 86 | ); 87 | 88 | test( 89 | test_title + 90 | 'customization is true and old theme dir is existed', 91 | function (t) { 92 | const TEST_PATH = path.resolve(__dirname, './fixture/theme/customization-true-old-themedir-exists'); 93 | process.chdir(TEST_PATH); 94 | const option = theme({ theme: 'oceandeep', customization: true }); 95 | let result = ''; 96 | if (path.sep !== '/') { 97 | result = 'hello world\r\n'; 98 | } else { 99 | result = 'hello world\n'; 100 | } 101 | t.equal(option.templates.page(), result); 102 | const oldDir = path.resolve( 103 | __dirname, 104 | './fixture/theme/customization-true-old-themedir-exists', 105 | 'themes' + path.sep + 'oceandeep' 106 | ); 107 | const newDir = path.resolve( 108 | __dirname, 109 | './fixture/theme/customization-true-old-themedir-exists', 110 | THEMEDIR 111 | ); 112 | fs.moveSync(newDir, oldDir); 113 | t.end(); 114 | } 115 | ); 116 | 117 | test( 118 | test_title + 119 | 'customization is true and no theme dir exists', 120 | function (t) { 121 | const TEST_PATH = path.resolve(__dirname, './fixture/theme/customization-true-no-themedir-exists'); 122 | const absoluteThemeDir = path.resolve( 123 | __dirname, 124 | './fixture/theme/customization-true-no-themedir-exists', 125 | THEMEDIR 126 | ); 127 | process.chdir(TEST_PATH); 128 | const option = theme({ theme: 'oceandeep', customization: true }); 129 | t.equal(fs.existsSync(absoluteThemeDir), true); 130 | t.equal(typeof option.templates.page, 'function'); 131 | fs.removeSync(absoluteThemeDir); 132 | t.end(); 133 | } 134 | ); 135 | 136 | test( 137 | test_title + 138 | 'customization is false', 139 | function (t) { 140 | const absoluteThemeDir = path.resolve(__dirname, THEMEDIR); 141 | const option = theme({ theme: 'oceandeep', customization: false }); 142 | t.equal(fs.existsSync(absoluteThemeDir), false); 143 | t.equal(typeof option.templates.page, 'function'); 144 | t.end(); 145 | } 146 | ); 147 | -------------------------------------------------------------------------------- /test/utils/isHomepage.test.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const test = require('tape'); 4 | const isHomepage = require('../../utils/isHomepage'); 5 | 6 | const log = require('../../lib/utils').log; 7 | log.setLevel('error'); 8 | 9 | const test_title = '[util/isHomepage.js] '; 10 | 11 | test( 12 | test_title + 13 | 'if current path is /, return true', 14 | function (t) { 15 | const result = isHomepage('/'); 16 | 17 | t.equal(result, true); 18 | t.end(); 19 | } 20 | ); 21 | 22 | test( 23 | test_title + 24 | 'if current path is a.md, return false', 25 | function (t) { 26 | const result = isHomepage('a.md'); 27 | 28 | t.equal(result, false); 29 | t.end(); 30 | } 31 | ); 32 | -------------------------------------------------------------------------------- /test/utils/makeChapterList.test.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const test = require('tape'); 4 | const makeChapterList = require('../../utils/makeChapterList'); 5 | 6 | const log = require('../../lib/utils').log; 7 | log.setLevel('error'); 8 | 9 | const test_title = '[utils/makeChapterList.js] '; 10 | 11 | test( 12 | test_title + 13 | 'empty ChapterArray', 14 | function (t) { 15 | const chapters = []; 16 | const result = makeChapterList('/', { chapters, relative_root_path: '' }); 17 | t.equal(result.chapterList, ''); 18 | t.end(); 19 | } 20 | ); 21 | 22 | test( 23 | test_title + 24 | 'top level article', 25 | function (t) { 26 | const chapters = [{'a.md': 'a.md'}]; 27 | const result = makeChapterList('/', { chapters, relative_root_path: '' }); 28 | t.equal(result.chapterList, ''); 29 | t.end(); 30 | } 31 | ); 32 | 33 | test( 34 | test_title + 35 | 'second level article', 36 | function (t) { 37 | const chapters = [{'a.md': 'a.md'}, {'test/': 'test'}, {'test/b.md': 'b.md'}]; 38 | const result = makeChapterList('/', { chapters, relative_root_path: '' }); 39 | t.equal(result.chapterList, ''); 40 | t.end(); 41 | } 42 | ); 43 | 44 | test( 45 | test_title + 46 | 'third level article', 47 | function (t) { 48 | const chapters = [ 49 | {'a.md': 'a.md'}, 50 | {'test/': 'test'}, 51 | {'test/b.md': 'b.md'}, 52 | {'test/c/': 'c'}, 53 | {'test/c/d.md': 'd.md'} 54 | ]; 55 | const result = makeChapterList('/', { chapters, relative_root_path: '' }); 56 | t.equal( 57 | result.chapterList, 58 | '' 59 | ); 60 | t.end(); 61 | } 62 | ); 63 | 64 | test( 65 | test_title + 66 | 'complex level case one', 67 | function (t) { 68 | const chapters = [ 69 | {'test/': 'xxx'}, 70 | {'test/a.md': 'xxx'}, 71 | {'develop/': 'xxx'}, 72 | {'develop/front/': 'xxx'}, 73 | {'develop/front/1.md': 'xxx'} 74 | ]; 75 | const result = makeChapterList('/', { chapters, relative_root_path: '' }); 76 | t.equal( 77 | result.chapterList, 78 | '' 79 | ); 80 | t.end(); 81 | } 82 | ); 83 | 84 | test( 85 | test_title + 86 | 'complex level case two', 87 | function (t) { 88 | const chapters = [ 89 | {'test/': 'xxx'}, 90 | {'test/a.md': 'xxx'}, 91 | {'develop/': 'xxx'}, 92 | {'develop/front/': 'xxx'}, 93 | {'develop/front/1.md': 'xxx'}, 94 | {'develop/front/2.md': 'xxx'}, 95 | {'develop/back/': 'xxx'}, 96 | {'devlelop/back/3/': 'xxx'}, 97 | {'devlelop/back/3/3.md': 'xxx'}, 98 | ]; 99 | const result = makeChapterList('/', { chapters, relative_root_path: '' }); 100 | t.equal( 101 | result.chapterList, 102 | '' 103 | ); 104 | t.end(); 105 | } 106 | ); 107 | 108 | test( 109 | test_title + 110 | 'complex level case three', 111 | function (t) { 112 | const chapters = [ 113 | {'test/': 'xxx'}, 114 | {'test/a.md': 'xxx'}, 115 | {'develop/': 'xxx'}, 116 | {'develop/front/': 'xxx'}, 117 | {'develop/front/1.md': 'xxx'}, 118 | {'develop/front/2.md': 'xxx'}, 119 | {'develop/back/': 'xxx'}, 120 | {'devlelop/back/3/': 'xxx'}, 121 | {'devlelop/back/3/3.md': 'xxx'}, 122 | {'devlelop/back/4/': 'xxx'}, 123 | {'devlelop/back/4/4.md': 'xxx'}, 124 | ]; 125 | const result = makeChapterList('/', { chapters, relative_root_path: '' }); 126 | t.equal( 127 | result.chapterList, 128 | '' 129 | ); 130 | t.end(); 131 | } 132 | ); 133 | 134 | test( 135 | test_title + 136 | 'complex level case three', 137 | function (t) { 138 | const chapters = [ 139 | {'test/': 'xxx'}, 140 | {'test/a.md': 'xxx'}, 141 | {'develop/': 'xxx'}, 142 | {'develop/front/': 'xxx'}, 143 | {'develop/front/1.md': 'xxx'}, 144 | {'develop/front/2.md': 'xxx'}, 145 | {'develop/back/': 'xxx'}, 146 | {'devlelop/back/3/': 'xxx'}, 147 | {'devlelop/back/3/3.md': 'xxx'}, 148 | {'devlelop/back/4/': 'xxx'}, 149 | {'devlelop/back/4/4.md': 'xxx'}, 150 | {'other/': 'xxx'}, 151 | {'other/5.md': 'xxx'}, 152 | ]; 153 | const result = makeChapterList('/', { chapters, relative_root_path: '' }); 154 | t.equal( 155 | result.chapterList, 156 | '' 157 | ); 158 | t.end(); 159 | } 160 | ); 161 | -------------------------------------------------------------------------------- /test/utils/makeChaptersOrigin.test.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const test = require('tape'); 4 | const makeChaptersOrigin = require('../../utils/makeChaptersOrigin'); 5 | 6 | const log = require('../../lib/utils').log; 7 | log.setLevel('error'); 8 | 9 | const test_title = '[utils/makeChaptersOrigin.js] '; 10 | 11 | test( 12 | test_title + 13 | 'nextLevelBegins / currentLevelEnds / currentLevelEndNumber No.1', 14 | function (t) { 15 | const chapters = [ 16 | { 'a.md': 'xxx' }, 17 | { 'dir1/': 'xxx' }, 18 | { 'dir1/dir2/': 'xxx' }, 19 | { 'dir1/dir2/b.md': 'xxx' }, 20 | { 'dir3/': 'xxx' }, 21 | { 'dir3/c.md': 'xxx' }, 22 | { 'd.md': 'xxx' } 23 | ]; 24 | const result = makeChaptersOrigin(chapters); 25 | t.equal(result[0].nextLevelBegins, false); 26 | t.equal(result[0].currentLevelEnds, false); 27 | t.equal(result[1].nextLevelBegins, true); 28 | t.equal(result[1].currentLevelEnds, false); 29 | t.equal(result[2].nextLevelBegins, true); 30 | t.equal(result[2].currentLevelEnds, false); 31 | t.equal(result[3].nextLevelBegins, false); 32 | t.equal(result[3].currentLevelEnds, true); 33 | t.equal(result[3].currentLevelEndNum, 2); 34 | t.equal(result[4].nextLevelBegins, true); 35 | t.equal(result[4].currentLevelEnds, false); 36 | t.equal(result[5].nextLevelBegins, false); 37 | t.equal(result[5].currentLevelEnds, true); 38 | t.equal(result[5].currentLevelEndNum, 1); 39 | t.equal(result[6].nextLevelBegins, false); 40 | t.equal(result[6].currentLevelEnds, false); 41 | t.end(); 42 | } 43 | ); 44 | 45 | test( 46 | test_title + 47 | 'nextLevelBegins / currentLevelEnds / currentLevelEndNumber No.2', 48 | function (t) { 49 | const chapters = [ 50 | { 'dir1/': 'xxx' }, 51 | { 'dir1/dir2/': 'xxx' }, 52 | { 'dir1/dir2/b.md': 'xxx' }, 53 | { 'dir1/dir2/c.md': 'xxx' } 54 | ]; 55 | const result = makeChaptersOrigin(chapters); 56 | t.equal(result[0].nextLevelBegins, true); 57 | t.equal(result[0].currentLevelEnds, false); 58 | t.equal(result[1].nextLevelBegins, true); 59 | t.equal(result[1].currentLevelEnds, false); 60 | t.equal(result[2].nextLevelBegins, false); 61 | t.equal(result[2].currentLevelEnds, false); 62 | t.equal(result[3].nextLevelBegins, false); 63 | t.equal(result[3].currentLevelEnds, true); 64 | t.equal(result[3].currentLevelEndNum, 2); 65 | t.end(); 66 | } 67 | ); 68 | 69 | test( 70 | test_title + 71 | 'nextLevelBegins / currentLevelEnds / currentLevelEndNumber No.3', 72 | function (t) { 73 | const chapters = [ 74 | { 'a.md': 'xxx' }, 75 | { 'b.md': 'xxx' }, 76 | { 'c.md': 'xxx' } 77 | ]; 78 | const result = makeChaptersOrigin(chapters); 79 | t.equal(result[0].nextLevelBegins, false); 80 | t.equal(result[0].currentLevelEnds, false); 81 | t.equal(result[1].nextLevelBegins, false); 82 | t.equal(result[1].currentLevelEnds, false); 83 | t.equal(result[2].nextLevelBegins, false); 84 | t.equal(result[2].currentLevelEnds, false); 85 | t.end(); 86 | } 87 | ); 88 | 89 | test( 90 | test_title + 91 | 'nextLevelBegins / currentLevelEnds / currentLevelEndNumber No.4', 92 | function (t) { 93 | const chapters = [ 94 | { 'dir1/': 'xxx' }, 95 | { 'dir1/a.md': 'xxx' }, 96 | { 'dir1/dir2/': 'xxx' }, 97 | { 'dir1/dir2/dir3/': 'xxx' }, 98 | { 'dir1/dir2/dir3/b.md': 'xxx' } 99 | ]; 100 | const result = makeChaptersOrigin(chapters); 101 | t.equal(result[0].nextLevelBegins, true); 102 | t.equal(result[0].currentLevelEnds, false); 103 | t.equal(result[1].nextLevelBegins, false); 104 | t.equal(result[1].currentLevelEnds, false); 105 | t.equal(result[2].nextLevelBegins, true); 106 | t.equal(result[2].currentLevelEnds, false); 107 | t.equal(result[3].nextLevelBegins, true); 108 | t.equal(result[3].currentLevelEnds, false); 109 | t.equal(result[4].nextLevelBegins, false); 110 | t.equal(result[4].currentLevelEnds, true); 111 | t.equal(result[4].currentLevelEndNum, 3); 112 | t.end(); 113 | } 114 | ); 115 | 116 | test( 117 | test_title + 118 | 'nextLevelBegins / currentLevelEnds / currentLevelEndNumber No.5', 119 | function (t) { 120 | const chapters = [ 121 | { 'dir1/': 'xxx' }, 122 | { 'dir1/a.md': 'xxx' }, 123 | { 'dir1/dir2/': 'xxx' }, 124 | { 'dir1/dir2/dir3/': 'xxx' }, 125 | { 'dir1/dir2/dir3/b.md': 'xxx' }, 126 | { 'dir4/': 'xxx' }, 127 | { 'dir4/c.md': 'xxx' } 128 | ]; 129 | const result = makeChaptersOrigin(chapters); 130 | t.equal(result[5].nextLevelBegins, true); 131 | t.equal(result[5].currentLevelEnds, false); 132 | t.equal(result[6].nextLevelBegins, false); 133 | t.equal(result[6].currentLevelEnds, true); 134 | t.equal(result[6].currentLevelEndNum, 1); 135 | t.end(); 136 | } 137 | ); 138 | 139 | test( 140 | test_title + 141 | 'nextLevelBegins / currentLevelEnds / currentLevelEndNumber No.5', 142 | function (t) { 143 | const chapters = [ 144 | { 'dir1/': 'xxx' }, 145 | { 'dir1/a.md': 'xxx' }, 146 | { 'dir1/b.md': 'xxx' }, 147 | { 'dir2/': 'xxx' }, 148 | { 'dir2/c.md': 'xxx' }, 149 | { 'dir3/': 'xxx' }, 150 | { 'dir3/d.md': 'xxx' } 151 | ]; 152 | const result = makeChaptersOrigin(chapters); 153 | t.equal(result[0].nextLevelBegins, true); 154 | t.equal(result[0].currentLevelEnds, false); 155 | t.equal(result[1].nextLevelBegins, false); 156 | t.equal(result[1].currentLevelEnds, false); 157 | t.equal(result[2].nextLevelBegins, false); 158 | t.equal(result[2].currentLevelEnds, true); 159 | t.equal(result[2].currentLevelEndNum, 1); 160 | t.equal(result[3].nextLevelBegins, true); 161 | t.equal(result[3].currentLevelEnds, false); 162 | t.equal(result[4].nextLevelBegins, false); 163 | t.equal(result[4].currentLevelEnds, true); 164 | t.equal(result[4].currentLevelEndNum, 1); 165 | t.equal(result[5].nextLevelBegins, true); 166 | t.equal(result[5].currentLevelEnds, false); 167 | t.equal(result[6].nextLevelBegins, false); 168 | t.equal(result[6].currentLevelEnds, true); 169 | t.equal(result[6].currentLevelEndNum, 1); 170 | t.end(); 171 | } 172 | ); 173 | 174 | test( 175 | test_title + 176 | 'nextLevelBegins / currentLevelEnds / currentLevelEndNumber No.6', 177 | function (t) { 178 | const chapters = [ 179 | { 'dir1/': 'xxx' }, 180 | { 'dir1/dir2/': 'xxx' }, 181 | { 'dir1/dir2/b.md': 'xxx' }, 182 | { 'dir1/dir2/c.md': 'xxx' }, 183 | { 'dir1/d.md': 'xxx' } 184 | ]; 185 | const result = makeChaptersOrigin(chapters); 186 | t.equal(result[3].nextLevelBegins, false); 187 | t.equal(result[3].currentLevelEnds, true); 188 | t.equal(result[3].currentLevelEndNum, 1); 189 | t.equal(result[4].nextLevelBegins, false); 190 | t.equal(result[4].currentLevelEnds, true); 191 | t.equal(result[4].currentLevelEndNum, 1); 192 | t.end(); 193 | } 194 | ); 195 | 196 | test( 197 | test_title + 198 | 'nextLevelBegins / currentLevelEnds / currentLevelEndNumber No.7', 199 | function (t) { 200 | const chapters = [ 201 | { 'dir1/': 'xxx' }, 202 | { 'dir1/dir2/': 'xxx' }, 203 | { 'dir1/dir2/b.md': 'xxx' }, 204 | { 'dir1/dir2/c.md': 'xxx' }, 205 | { 'dir1/d.md': 'xxx' }, 206 | { 'e.md': 'xxx' }, 207 | ]; 208 | const result = makeChaptersOrigin(chapters); 209 | t.equal(result[3].nextLevelBegins, false); 210 | t.equal(result[3].currentLevelEnds, true); 211 | t.equal(result[3].currentLevelEndNum, 1); 212 | t.equal(result[4].nextLevelBegins, false); 213 | t.equal(result[4].currentLevelEnds, true); 214 | t.equal(result[4].currentLevelEndNum, 1); 215 | t.equal(result[5].nextLevelBegins, false); 216 | t.equal(result[5].currentLevelEnds, false); 217 | t.end(); 218 | } 219 | ); 220 | -------------------------------------------------------------------------------- /test/utils/siteId.test.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const test = require('tape'); 4 | const getSiteId = require('../../utils/siteId'); 5 | 6 | const log = require('../../lib/utils').log; 7 | log.setLevel('error'); 8 | 9 | const test_title = '[util/siteId.js] '; 10 | 11 | test( 12 | test_title + 13 | 'return current dir name', 14 | function (t) { 15 | const originDir = process.cwd(); 16 | process.chdir(__dirname); 17 | const result = getSiteId(); 18 | t.equal(result, 'utils'); 19 | process.chdir(originDir); 20 | t.end(); 21 | } 22 | ); 23 | -------------------------------------------------------------------------------- /test/writePage.test.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const path = require('path'); 4 | const fs = require('fs-extra'); 5 | const test = require('tape'); 6 | // const yaml = require('js-yaml'); 7 | const _ = require('lodash'); 8 | const writePage = require('../lib/utils').writePage; 9 | 10 | const log = require('../lib/utils').log; 11 | log.setLevel('error'); 12 | 13 | const test_title = '[utils.js/writePage] '; 14 | 15 | test( 16 | test_title + 17 | 'simple write', 18 | function (t) { 19 | const TEST_PATH = path.resolve(__dirname, './fixture/writePage/simple-write'); 20 | process.chdir(TEST_PATH); 21 | const opt = { 22 | output: 'dist', 23 | content: '

My Title

Hello World

', 24 | templates: {} 25 | }; 26 | 27 | opt.templates.page = _.template('<%= content %>'); 28 | 29 | writePage('/', opt); 30 | 31 | t.ok(fs.existsSync(path.join(TEST_PATH, opt.output, 'index.html'))); 32 | t.equal( 33 | fs.readFileSync(path.join(TEST_PATH, opt.output, 'index.html'), 'utf8'), 34 | '

My Title

Hello World

' 35 | ); 36 | t.end(); 37 | fs.removeSync(path.join(TEST_PATH, opt.output)); 38 | } 39 | ); 40 | -------------------------------------------------------------------------------- /utils/escapeTitle.js: -------------------------------------------------------------------------------- 1 | const { escape } = require('lodash'); 2 | 3 | function escapeTitle(chapters) { 4 | if (!chapters) return undefined; 5 | chapters.map(c => { 6 | for (let key in c) { 7 | c[key] = escape(c[key]); 8 | } 9 | return c; 10 | }); 11 | return chapters; 12 | } 13 | 14 | module.exports = escapeTitle; 15 | -------------------------------------------------------------------------------- /utils/findRootPosition.js: -------------------------------------------------------------------------------- 1 | function findRootPosition(root, optionObj) { 2 | for (let i = 0; i < optionObj.chapters.length; i += 1) { 3 | if (optionObj.chapters[i][root]) { 4 | return i; 5 | } 6 | } 7 | throw new Error('Cannot find the path in Chapters!'); 8 | } 9 | 10 | module.exports = findRootPosition; 11 | -------------------------------------------------------------------------------- /utils/isHomepage.js: -------------------------------------------------------------------------------- 1 | function isHomepage(root) { 2 | if (root === '/' || root === 'index.md') return true; 3 | return false; 4 | } 5 | 6 | module.exports = isHomepage; 7 | -------------------------------------------------------------------------------- /utils/makeBreadcrumb.js: -------------------------------------------------------------------------------- 1 | function makeBreadcrumb(root, optionObj) { 2 | const bcHTMLArr = []; 3 | optionObj.breadcrumbOrigin.forEach(function (c) { 4 | let bcPath = c.path; 5 | /* 6 | if (bcPath.substr(-3) === '.md') { 7 | bcPath = bcPath.substr(0, bcPath.length - 3) + '.html'; 8 | } else if (bcPath.substr(-1) === '/') { 9 | bcPath += 'index.html'; 10 | } 11 | */ 12 | const str = '' 16 | + c.text 17 | + ''; 18 | bcHTMLArr.push(str); 19 | }); 20 | let bcHTML = ''; 23 | optionObj.breadcrumb = bcHTML; 24 | return optionObj; 25 | } 26 | 27 | module.exports = makeBreadcrumb; 28 | -------------------------------------------------------------------------------- /utils/makeBreadcrumbOrigin.js: -------------------------------------------------------------------------------- 1 | const findRootPosition = require('./findRootPosition'); 2 | 3 | function transform(chapterObj) { 4 | let key = Object.keys(chapterObj)[0]; 5 | let value = chapterObj[key]; 6 | 7 | if (key.substr(-3).toLowerCase() === '.md') { 8 | key = key.substr(0, key.length - 3) + '.html'; 9 | } 10 | 11 | if (key.substr(-1) === '/') { 12 | key += 'index.html'; 13 | } 14 | 15 | return { 16 | path: key, 17 | text: value 18 | }; 19 | } 20 | 21 | function makeBreadcrumbOrigin(root, optionObj) { 22 | const breadcrumbArr = [transform({ 'index.md': 'Home' })]; 23 | if (root !== '/') { 24 | const pathArr = root.split('/'); 25 | let currentPath = ''; 26 | for (let i = 0; i < pathArr.length - 1; i += 1) { 27 | currentPath += pathArr[i] + '/'; 28 | for (let m = 0; m < optionObj.chapters.length; m += 1) { 29 | if (optionObj.chapters[m][currentPath]) { 30 | breadcrumbArr.push(transform(optionObj.chapters[m])); 31 | break; 32 | } 33 | } 34 | } 35 | } 36 | 37 | if (root[root.length - 1] !== '/') { 38 | if (root.substr(-8) === 'index.md') { 39 | breadcrumbArr.pop(); 40 | } 41 | const position = findRootPosition(root, optionObj); 42 | breadcrumbArr.push(transform(optionObj.chapters[position])); 43 | } 44 | optionObj.breadcrumbOrigin = breadcrumbArr; 45 | return optionObj; 46 | } 47 | 48 | module.exports = makeBreadcrumbOrigin; 49 | -------------------------------------------------------------------------------- /utils/makeChapterList.js: -------------------------------------------------------------------------------- 1 | function makeChapterList(root, optionObj) { 2 | let chapterArr = optionObj.chapters; 3 | if (!optionObj.chapters) { 4 | chapterArr = []; 5 | } 6 | let str = ''; 23 | } 24 | } 25 | if (level === current_level && chapterPathOrigin.split('/')[level] === '') { 26 | str += ''; 27 | } 28 | 29 | let isFirstLevelDir = false; 30 | let isCurrentFirstLevelDir = false; 31 | if (level === 1 && chapterPathOrigin.split('/')[level] === '') { 32 | isFirstLevelDir = true; 33 | if (root.indexOf(Object.keys(c)[0]) === 0) { 34 | isCurrentFirstLevelDir = true; 35 | } 36 | } 37 | 38 | str += '
  • '; 39 | str += '' 43 | + c[Object.keys(c)[0]] 44 | + ''; 45 | if (isFirstLevelDir) { 46 | str += ' '; 51 | } 52 | str += '
  • '; 53 | 54 | if ( 55 | level > current_level || 56 | (level < current_level && chapterPathOrigin.split('/')[level] === '') || 57 | (level === current_level && chapterPathOrigin.split('/')[level] === '') 58 | ) { 59 | let collapseClass = ''; 60 | if (isCurrentFirstLevelDir /* level === 1 && root.indexOf(Object.keys(c)[0]) === 0 */) { 61 | collapseClass = ' chapter-level-1-current'; 62 | } 63 | str += ''; 68 | } 69 | } 70 | 71 | current_level = level; 72 | }); 73 | str += ''; 74 | optionObj.chapterList = str; 75 | return optionObj; 76 | } 77 | 78 | module.exports = makeChapterList; 79 | -------------------------------------------------------------------------------- /utils/makeChaptersOrigin.js: -------------------------------------------------------------------------------- 1 | const path = require('path'); 2 | 3 | function suffix(str) { 4 | if (str.substr(-1) === '/') { 5 | return str + 'index.html'; 6 | } 7 | if (str.substr(-3).toLowerCase() === '.md') { 8 | return str.substr(0, str.length - 3) + '.html'; 9 | } 10 | return str; 11 | } 12 | 13 | function compare(current, previous) { 14 | const currentArr = path.dirname(current).split('/'); 15 | const previousArr = path.dirname(previous).split('/'); 16 | let num = previousArr.length; 17 | for (let i = 0; i < previousArr.length; i++) { 18 | if (currentArr[i] === undefined) break; 19 | if (previousArr[i] === currentArr[i]) { 20 | num -= 1; 21 | } else { 22 | break; 23 | } 24 | } 25 | return num; 26 | } 27 | 28 | function makeChaptersOrigin(chapters) { 29 | if (!chapters) return undefined; 30 | const arr = []; 31 | chapters.forEach(i => { 32 | const obj = {}; 33 | const key = Object.keys(i)[0]; 34 | obj.origin = key; 35 | obj.path = suffix(key); 36 | obj.text = i[key]; 37 | arr.push(obj); 38 | }); 39 | 40 | // option.nextLevelBegins 41 | arr.forEach(i => { 42 | if (i.origin.substr(-1) === '/') { 43 | i.nextLevelBegins = true; 44 | } else { 45 | i.nextLevelBegins = false; 46 | } 47 | }); 48 | 49 | // option.currentLevelEnds 50 | // option.currentLevelEndNum 51 | let previousDir = '.'; 52 | let previousDepth = 1; 53 | let openLevel = 0; 54 | arr.forEach((item, index) => { 55 | const currentDir = path.dirname(item.path); 56 | if (index === 0) { 57 | item.currentLevelEnds = false; 58 | previousDir = currentDir; 59 | if (item.origin.substr(-1) === '/') { 60 | previousDepth = 2; 61 | openLevel = 1; 62 | } 63 | return; 64 | } 65 | 66 | if (currentDir === previousDir) { // not change dir 67 | item.currentLevelEnds = false; 68 | if (index === arr.length - 1) { 69 | if (currentDir !== '.') { 70 | item.currentLevelEnds = true; 71 | item.currentLevelEndNum = openLevel; 72 | } else { 73 | item.currentLevelEnds = false; 74 | } 75 | } 76 | } else { // change dir 77 | if (item.path.split('/').length > previousDepth) { // previous dir's next level 78 | item.currentLevelEnds = false; 79 | openLevel += 1; 80 | } else if (item.path.split('/').length === previousDepth) { // previous dir's same level 81 | item.currentLevelEnds = false; 82 | arr[index - 1].currentLevelEnds = true; 83 | arr[index - 1].currentLevelEndNum = 1; 84 | } else { // previous dir's top level 85 | item.currentLevelEnds = false; 86 | arr[index - 1].currentLevelEnds = true; 87 | arr[index - 1].currentLevelEndNum = compare(item.path, arr[index - 1].path); 88 | openLevel = currentDir.split('/').length; 89 | if (index === arr.length - 1) { 90 | if (currentDir !== '.') { 91 | item.currentLevelEnds = true; 92 | item.currentLevelEndNum = openLevel; 93 | } else { 94 | item.currentLevelEnds = false; 95 | } 96 | } 97 | } 98 | } 99 | previousDir = currentDir; 100 | previousDepth = item.path.split('/').length; 101 | }); 102 | return arr; 103 | } 104 | 105 | module.exports = makeChaptersOrigin; 106 | -------------------------------------------------------------------------------- /utils/makeNextPageObject.js: -------------------------------------------------------------------------------- 1 | const findRootPosition = require('./findRootPosition'); 2 | 3 | function makeNextPageObject(root, optionObj) { 4 | const chapters = optionObj.chapters; 5 | 6 | if (!chapters || !optionObj.chapters.length) { 7 | optionObj.next_page_object = null; 8 | return optionObj; 9 | } 10 | 11 | if (root === '/') { 12 | if (chapters[0]['index.md']) { 13 | if (chapters.length === 1) { 14 | optionObj.next_page_object = null; 15 | } else { 16 | optionObj.next_page_object = chapters[1]; 17 | } 18 | } else { 19 | optionObj.next_page_object = chapters[0]; 20 | } 21 | return optionObj; 22 | } 23 | 24 | const position = findRootPosition(root, optionObj); 25 | if (position === (chapters.length - 1)) { 26 | optionObj.next_page_object = null; 27 | } else if ( 28 | root.substr(-1) === '/' && 29 | Object.keys(chapters[position + 1])[0].substr(-8) === 'index.md' 30 | ) { 31 | optionObj.next_page_object = chapters[position + 2]; 32 | } else { 33 | optionObj.next_page_object = chapters[position + 1]; 34 | } 35 | 36 | return optionObj; 37 | } 38 | 39 | module.exports = makeNextPageObject; 40 | -------------------------------------------------------------------------------- /utils/makeNextPageOrigin.js: -------------------------------------------------------------------------------- 1 | const makeNextPageObject = require('./makeNextPageObject'); 2 | 3 | function suffix(str) { 4 | if (str.substr(-1) === '/') { 5 | return str + 'index.html'; 6 | } 7 | if (str.substr(-3).toLowerCase() === '.md') { 8 | return str.substr(0, str.length - 3) + '.html'; 9 | } 10 | return str; 11 | } 12 | 13 | function makeNextPageOrigin(root, optionObj) { 14 | if (optionObj.next_page_object === undefined) { 15 | optionObj = makeNextPageObject(root, optionObj); 16 | } 17 | 18 | if (optionObj.next_page_object === null) { 19 | optionObj.nextPageOrigin = null; 20 | } else { 21 | const obj = {}; 22 | const key = Object.keys(optionObj.next_page_object)[0]; 23 | obj.origin = key; 24 | obj.path = suffix(key); 25 | obj.text = optionObj.next_page_object[key]; 26 | optionObj.nextPageOrigin = obj; 27 | } 28 | 29 | return optionObj; 30 | } 31 | 32 | module.exports = makeNextPageOrigin; 33 | -------------------------------------------------------------------------------- /utils/makePreviousPageObject.js: -------------------------------------------------------------------------------- 1 | const findRootPosition = require('./findRootPosition'); 2 | 3 | function makePreviousPageObject(root, optionObj) { 4 | if (root === '/') { 5 | optionObj.previous_page_object = null; 6 | return optionObj; 7 | } 8 | 9 | const position = findRootPosition(root, optionObj); 10 | if (position === 0) { 11 | if (optionObj.chapters[0]['index.md']) { 12 | optionObj.previous_page_object = null; 13 | } else { 14 | optionObj.previous_page_object = { 'index.md': 'Home' }; 15 | } 16 | return optionObj; 17 | } 18 | 19 | if (root.substr(-8) === 'index.md') { 20 | optionObj.previous_page_object = optionObj.chapters[position - 2]; 21 | return optionObj; 22 | } 23 | optionObj.previous_page_object = optionObj.chapters[position - 1]; 24 | return optionObj; 25 | } 26 | 27 | module.exports = makePreviousPageObject; 28 | -------------------------------------------------------------------------------- /utils/makePreviousPageOrigin.js: -------------------------------------------------------------------------------- 1 | const makePreviousPageObject = require('./makePreviousPageObject'); 2 | 3 | function suffix(str) { 4 | if (str.substr(-1) === '/') { 5 | return str + 'index.html'; 6 | } 7 | if (str.substr(-3).toLowerCase() === '.md') { 8 | return str.substr(0, str.length - 3) + '.html'; 9 | } 10 | return str; 11 | } 12 | 13 | function makePreviousPageOrigin(root, optionObj) { 14 | if (optionObj.previous_page_object === undefined) { 15 | optionObj = makePreviousPageObject(root, optionObj); 16 | } 17 | 18 | if (optionObj.previous_page_object === null) { 19 | optionObj.previousPageOrigin = null; 20 | } else { 21 | const obj = {}; 22 | const key = Object.keys(optionObj.previous_page_object)[0]; 23 | obj.origin = key; 24 | obj.path = suffix(key); 25 | obj.text = optionObj.previous_page_object[key]; 26 | optionObj.previousPageOrigin = obj; 27 | } 28 | 29 | return optionObj; 30 | } 31 | 32 | module.exports = makePreviousPageOrigin; 33 | -------------------------------------------------------------------------------- /utils/readmeCheck.js: -------------------------------------------------------------------------------- 1 | const path = require('path'); 2 | const fs = require('fs-extra'); 3 | 4 | function readmeCheck(dir = process.cwd()) { 5 | const readme1 = path.resolve(dir, 'readme.md'); 6 | const readme2 = path.resolve(dir, 'README.md'); 7 | const readme3 = path.resolve(dir, 'README.MD'); 8 | if ( 9 | fs.existsSync(readme1) || 10 | fs.existsSync(readme2) || 11 | fs.existsSync(readme3) 12 | ) return true; 13 | return false; 14 | } 15 | 16 | module.exports = readmeCheck; 17 | -------------------------------------------------------------------------------- /utils/siteId.js: -------------------------------------------------------------------------------- 1 | const path = require('path'); 2 | 3 | function getSiteId() { 4 | const currentDir = process.cwd(); 5 | const currentDirArr = currentDir.split(path.sep); 6 | let dir = currentDirArr[currentDirArr.length - 1]; 7 | if (dir.substr(-9) === '-tutorial') dir = dir.substr(0, dir.length - 9); 8 | return dir; 9 | } 10 | 11 | module.exports = getSiteId; 12 | -------------------------------------------------------------------------------- /utils/themePath.js: -------------------------------------------------------------------------------- 1 | const path = require('path'); 2 | 3 | function themePath(themeName) { 4 | let theme_src = ''; 5 | try { 6 | const modulePath = path.resolve(process.cwd(), 'node_modules', 'loppo-theme-' + themeName); 7 | theme_src = require(modulePath); 8 | } catch (e) { 9 | theme_src = require(path.resolve(__dirname, '../node_modules', 'loppo-theme-' + themeName)); 10 | } 11 | return theme_src; 12 | } 13 | 14 | module.exports = themePath; 15 | --------------------------------------------------------------------------------