├── .eslintrc.json ├── .gitignore ├── .jshintrc ├── README.md ├── docs ├── all.min.css ├── app.min.js ├── assets │ ├── avatar.png │ ├── logo.png │ ├── stars.png │ └── style.css ├── build.js ├── components.json ├── components.yaml ├── configuration.html ├── demos │ ├── comments │ │ ├── all.min.css │ │ ├── components.json │ │ ├── design-manual-components.json │ │ ├── index.html │ │ ├── index.js │ │ ├── index.md │ │ └── lib │ │ │ ├── foo1.html │ │ │ ├── foo2.html │ │ │ ├── foo3.html │ │ │ ├── foo4.html │ │ │ ├── foo5.html │ │ │ ├── foo6.html │ │ │ └── foo7.html │ └── scrape │ │ ├── all.min.css │ │ ├── components.json │ │ ├── components.yaml │ │ ├── design-manual-components.json │ │ ├── index.html │ │ ├── index.js │ │ ├── index.md │ │ └── lib │ │ ├── features.html │ │ ├── navigation.html │ │ ├── page-head.html │ │ └── text.html ├── design-manual-components.json ├── design-manual-config.json ├── examples.html ├── getting-started.html ├── index.html ├── lib │ └── hello-world.html └── src │ ├── Configuration.md │ ├── Examples.md │ ├── Getting Started.md │ ├── Index.md │ └── patterns.html ├── lib ├── components.js ├── constants.js ├── css.js ├── index.js ├── pages.js ├── puppeteer.js ├── static-analysis.js └── utils.js ├── package.json ├── template ├── styles │ ├── _vars.scss │ ├── all.scss │ ├── base │ │ ├── _base.scss │ │ └── _reset.scss │ └── components │ │ ├── _breadcrumbs.scss │ │ ├── _code.scss │ │ ├── _component.scss │ │ ├── _content.scss │ │ ├── _header.scss │ │ ├── _scrolltop.scss │ │ ├── _sidebar.scss │ │ └── _table-of-contents.scss └── templates │ ├── _component.pug │ ├── _table-of-contents.pug │ └── base.pug ├── test ├── autofill │ ├── autofill.md │ ├── components.md │ └── test.js ├── component-html-options │ ├── page.md │ └── test.js ├── components.json ├── components2.json ├── config │ ├── page.md │ ├── page2.md │ ├── test-body-html.js │ ├── test-component-body-html.js │ ├── test-component-head-html.js │ ├── test-component.js │ ├── test-contentsflag.js │ ├── test-error.js │ ├── test-head-html.js │ ├── test-meta.js │ ├── test-nav.js │ ├── test-new.js │ ├── test-output.js │ ├── test-prerender.js │ ├── test-same.js │ └── test-validate.js ├── defaults │ ├── components.md │ └── test.js ├── gulp │ └── test.js ├── interrupt │ ├── components.md │ └── test.js ├── page-html-options │ ├── page.md │ └── test.js ├── prerender │ ├── page.md │ └── test.js ├── puppeteer │ ├── 222.html │ └── test.js ├── render-css │ └── test.js ├── render-pages │ ├── code-frameless.md │ ├── code.md │ ├── component-frameless.md │ ├── component.md │ ├── page.md │ ├── test.js │ └── toc.md └── utils.js └── yarn.lock /.eslintrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "eslint:recommended", 3 | "parserOptions": { 4 | "ecmaVersion": 5, 5 | "sourceType": "module" 6 | }, 7 | "env": { 8 | "es6": false, 9 | "browser": true, 10 | "node": true 11 | }, 12 | "rules": { 13 | "eol-last": 0, 14 | "no-console": 0 15 | }, 16 | "plugins": [ 17 | "import" 18 | ] 19 | } -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Logs 2 | logs 3 | *.log 4 | 5 | # Runtime data 6 | pids 7 | *.pid 8 | *.seed 9 | 10 | # Directory for instrumented libs generated by jscoverage/JSCover 11 | lib-cov 12 | 13 | # Coverage directory used by tools like istanbul 14 | coverage 15 | 16 | # Grunt intermediate storage (http://gruntjs.com/creating-plugins#storing-task-files) 17 | .grunt 18 | 19 | # node-waf configuration 20 | .lock-wscript 21 | 22 | # Compiled binary addons (http://nodejs.org/api/addons.html) 23 | build/Release 24 | 25 | # Dependency directory 26 | # https://www.npmjs.org/doc/misc/npm-faq.html#should-i-check-my-node_modules-folder-into-git 27 | node_modules 28 | 29 | **/tmp 30 | .DS_STORE 31 | .vscode 32 | test/defaults/tmp/all.min.css 33 | test/interrupt/tmp/all.min.css 34 | -------------------------------------------------------------------------------- /.jshintrc: -------------------------------------------------------------------------------- 1 | { 2 | "browser": true, 3 | "node": true, 4 | "esnext": false, 5 | "bitwise": true, 6 | "curly": true, 7 | "immed": true, 8 | "newcap": true, 9 | "noarg": true, 10 | "undef": true, 11 | "unused": "vars", 12 | "strict": true 13 | } -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | 2 | 3 | Design Manual is a Design System Generator that can be used for documentation of your websites components and design decisions. It combines free format Markdown files with a json file containing your components and creates a beautiful design manual. 4 | 5 | https://aratramba.github.io/design-manual/ 6 | 7 | * [Getting Started](https://aratramba.github.io/design-manual/getting-started.html) 8 | * [Configuration](https://aratramba.github.io/design-manual/configuration.html) 9 | * [Examples](https://aratramba.github.io/design-manual/examples.html) 10 | 11 | --- 12 | 13 | Gather components from your website and embed them in your Design System. 14 | 15 | ```markdown 16 | # This is my Design System 17 | 18 | !{my-component} 19 | !{my-other-component} 20 | ``` 21 | -------------------------------------------------------------------------------- /docs/assets/avatar.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Aratramba/design-manual/db7feaa3d35b3324394f9134359e1923d51e9c17/docs/assets/avatar.png -------------------------------------------------------------------------------- /docs/assets/logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Aratramba/design-manual/db7feaa3d35b3324394f9134359e1923d51e9c17/docs/assets/logo.png -------------------------------------------------------------------------------- /docs/assets/stars.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Aratramba/design-manual/db7feaa3d35b3324394f9134359e1923d51e9c17/docs/assets/stars.png -------------------------------------------------------------------------------- /docs/assets/style.css: -------------------------------------------------------------------------------- 1 | .content { 2 | max-width: 1200px; 3 | margin: 0 auto; 4 | } 5 | 6 | .lead { 7 | padding: 100px 0; 8 | position: relative; 9 | text-align: center; 10 | } 11 | 12 | .lead img { 13 | height: auto; 14 | max-height: 100px; 15 | } 16 | 17 | .lead::before { 18 | pointer-events: none; 19 | content: ''; 20 | display: block; 21 | position: absolute; 22 | top: -35px; 23 | right: -20px; 24 | bottom: 0; 25 | left: -20px; 26 | background-image: url(./stars.png); 27 | background-repeat: no-repeat; 28 | opacity: .1; 29 | transform: rotate(-180deg); 30 | } 31 | 32 | .lead h1 { 33 | margin: 10px 0 0 0; 34 | font-size: 14.5px; 35 | color: #222; 36 | display: inline-block; 37 | margin-bottom: 40px; 38 | } 39 | 40 | .lead a { 41 | border-radius: 3px; 42 | padding: 20px 30px; 43 | font-size: 20px; 44 | display: inline-block; 45 | color: white; 46 | text-decoration: none; 47 | background-image: url(./stars.png); 48 | background-position: -346% -79%; 49 | background-color: #2A66DC; 50 | transition: background-color .15s, background-position .25s cubic-bezier(0.075, 0.820, 0.165, 1.000); 51 | } 52 | 53 | .lead a:hover { 54 | background-color: #225BCB; 55 | background-position: -363% -79%; 56 | } 57 | 58 | 59 | @media screen and (min-width: 768px) { 60 | .features { 61 | display: flex; 62 | } 63 | 64 | .features__item { 65 | flex: 1; 66 | } 67 | 68 | .features__item h2 { 69 | max-width: calc(100% - 20px); 70 | } 71 | 72 | .features__item p { 73 | max-width: calc(100% - 20px); 74 | } 75 | } -------------------------------------------------------------------------------- /docs/build.js: -------------------------------------------------------------------------------- 1 | const DesignManual = require('../lib/index'); 2 | 3 | const gatherComponents = require('gather-components'); 4 | const serveStatic = require('serve-static'); 5 | const http = require('http'); 6 | const cp = require('child_process'); 7 | 8 | const serve = serveStatic(__dirname); 9 | const server = http.createServer((req, res) => { 10 | serve(req, res, function (err) { 11 | res.statusCode = err ? (err.status || 500) : 404 12 | res.end(err ? err.stack : 'sorry!') 13 | }) 14 | }); 15 | 16 | server.on('listening', () => { 17 | 18 | gatherComponents({ 19 | url: 'http://localhost:8000/', 20 | paths: ['src/patterns.html'], 21 | components: 'docs/components.yaml', 22 | output: 'docs/components.json' 23 | }).then(() => { 24 | DesignManual.build({ 25 | force: true, 26 | output: 'docs/', 27 | pages: 'docs/src/', 28 | components: 'docs/components.json', 29 | nav: [ 30 | { label: '🏡', href: 'index.html' }, 31 | { label: 'Getting started', href: 'getting-started.html' }, 32 | { label: 'Configuration', href: 'configuration.html' }, 33 | { label: 'Examples', href: 'examples.html' }, 34 | { label: 'Github ↗', href: 'https://github.com/aratramba/design-manual', target: '_blank' }, 35 | ], 36 | meta: { 37 | domain: 'aratramba.github.io/design-manual/', 38 | title: 'Design Manual', 39 | avatar: './assets/avatar.png', 40 | version: 'v' + require('../package.json').version 41 | }, 42 | headHtml: ` 43 | 44 | `, 45 | bodyHtml: '', 46 | componentBodyHtml: ` 47 | 50 | `, 51 | prerender: { 52 | port: 3000, 53 | path: '/', 54 | serveFolder: 'docs/', 55 | }, 56 | onComplete: () => { 57 | let scraper = cp.fork(`${__dirname}/demos/scrape/index.js`); 58 | scraper.on('exit', () => { 59 | let collector = cp.fork(`${__dirname}/demos/comments/index.js`); 60 | collector.on('exit', () => { 61 | if (process.argv[2] === '-q') process.exit(); 62 | }); 63 | }) 64 | } 65 | }); 66 | }); 67 | }); 68 | 69 | server.listen(8000); -------------------------------------------------------------------------------- /docs/components.json: -------------------------------------------------------------------------------- 1 | [ 2 | { 3 | "meta": { 4 | "name": "hello world", 5 | "description": "" 6 | }, 7 | "output": "\"\"" 8 | } 9 | ] 10 | -------------------------------------------------------------------------------- /docs/components.yaml: -------------------------------------------------------------------------------- 1 | - 2 | name: hello world 3 | selector: img -------------------------------------------------------------------------------- /docs/demos/comments/components.json: -------------------------------------------------------------------------------- 1 | [{"meta":{"name":"foo7","capture":"all"},"file":"capture.html","output":"
1
\n
2
\n
3
\n
4
\n
5
\n
6
"},{"meta":{},"file":"capture.html","output":"
6
"},{"meta":{"name":"foo1","capture":3},"file":"capture.html","output":"
1
\n
2
\n
3
"},{"meta":{"name":"foo2","capture":-1},"file":"capture.html","output":""},{"meta":{"name":"foo3","capture":0},"file":"capture.html","output":""},{"meta":{"name":"foo4","capture":"section"},"file":"capture.html","output":"
4
\n
5
\n
6
"},{"meta":{"name":"foo5","capture":1},"file":"capture.html","output":"
7
"},{"meta":{"name":"foo6","capture":"section"},"file":"capture.html","output":"\n\n
8
"},{"meta":{"name":"foo6","capture":"all"},"file":"capture.html","output":"
1
\n
2
\n
3
\n
4
4b
\n
5\n \n
5b
\n
"},{"meta":{},"file":"capture.html","output":"
4
4b
"},{"meta":{},"file":"capture.html","output":"
5\n \n
5b
\n
"},{"meta":{},"file":"capture.html","output":"
5b
"}] -------------------------------------------------------------------------------- /docs/demos/comments/design-manual-components.json: -------------------------------------------------------------------------------- 1 | [{"meta":{"name":"foo7","capture":"all"},"file":"capture.html","output":"
1
\n
2
\n
3
\n
4
\n
5
\n
6
","dm":{"libFile":"./lib/foo7.html","height":116}},{"meta":{"name":"foo1","capture":3},"file":"capture.html","output":"
1
\n
2
\n
3
","dm":{"libFile":"./lib/foo1.html","height":68}},{"meta":{"name":"foo2","capture":-1},"file":"capture.html","output":"","dm":{"libFile":"./lib/foo2.html","height":20}},{"meta":{"name":"foo3","capture":0},"file":"capture.html","output":"","dm":{"libFile":"./lib/foo3.html","height":20}},{"meta":{"name":"foo4","capture":"section"},"file":"capture.html","output":"
4
\n
5
\n
6
","dm":{"libFile":"./lib/foo4.html","height":68}},{"meta":{"name":"foo5","capture":1},"file":"capture.html","output":"
7
","dm":{"libFile":"./lib/foo5.html","height":36}},{"meta":{"name":"foo6","capture":"section"},"file":"capture.html","output":"\n\n
8
","dm":{"libFile":"./lib/foo6.html","height":36}},{"meta":{"name":"foo6","capture":"all"},"file":"capture.html","output":"
1
\n
2
\n
3
\n
4
4b
\n
5\n \n
5b
\n
","dm":{"libFile":"./lib/foo6.html","height":132}}] -------------------------------------------------------------------------------- /docs/demos/comments/index.js: -------------------------------------------------------------------------------- 1 | const DesignManual = require('../../../lib/index'); 2 | const collectComponents = require('collect-components'); 3 | const rimraf = require('rimraf'); 4 | 5 | collectComponents({ 6 | url: 'https://raw.githubusercontent.com/aratramba/collect-components/master/test/fixtures/', // https://aratramba.github.io/design-manual/ 7 | paths: ['capture.html'], 8 | output: __dirname + '/components.json', 9 | complete: () => { 10 | DesignManual.build({ 11 | output: __dirname, 12 | pages: __dirname, 13 | components: __dirname + '/components.json', 14 | nav: [ 15 | { label: 'Homepage', href: 'index.html' } 16 | ], 17 | meta: { 18 | domain: 'aratramba.github.io/design-manual/', 19 | title: 'Demo' 20 | }, 21 | prerender: { 22 | port: 3000, 23 | path: 'demos/comments/', 24 | serveFolder: 'docs/' 25 | }, 26 | componentHeadHtml: ` 27 | 28 | 29 | 30 | `, 31 | onComplete: () => { 32 | process.exit(); 33 | } 34 | }); 35 | 36 | rimraf.sync(__dirname + '/design-manual-config.json'); 37 | } 38 | }); -------------------------------------------------------------------------------- /docs/demos/comments/index.md: -------------------------------------------------------------------------------- 1 | # Design Manual / Collect Components 2 | This page contains some components scraped from the Design Manual website using [Collect Components](https://www.npmjs.com/package/collect-components). 3 | 4 | --- 5 | 6 | ## Configuration 7 | 8 | * Components are defined in comments tagged [`@component`](https://raw.githubusercontent.com/aratramba/collect-components/master/test/fixtures/capture.html) 9 | * This page written in markdown: [index.md](https://raw.githubusercontent.com/aratramba/design-manual/master/docs/demos/comments/index.md) 10 | * The Node.js build script: [index.js](https://raw.githubusercontent.com/aratramba/design-manual/master/docs/demos/comments/index.js) 11 | 12 | --- 13 | 14 | ## Components 15 | ### Contents 16 | !{foo1} 17 | !{foo2} 18 | !{foo3} 19 | !{foo4} 20 | !{foo5} 21 | !{foo6} 22 | !{foo7} -------------------------------------------------------------------------------- /docs/demos/comments/lib/foo1.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | foo1 - Demo - aratramba.github.io/design-manual/ 7 | 8 | 9 | 10 | 11 | 33 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 |
46 |
1
47 |
2
48 |
3
49 |
50 | 51 | 56 | 57 | 58 | -------------------------------------------------------------------------------- /docs/demos/comments/lib/foo2.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | foo2 - Demo - aratramba.github.io/design-manual/ 7 | 8 | 9 | 10 | 11 | 33 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 |
46 | 47 |
48 | 49 | 52 | 53 | 54 | -------------------------------------------------------------------------------- /docs/demos/comments/lib/foo3.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | foo3 - Demo - aratramba.github.io/design-manual/ 7 | 8 | 9 | 10 | 11 | 33 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 |
46 | 47 |
48 | 49 | 52 | 53 | 54 | -------------------------------------------------------------------------------- /docs/demos/comments/lib/foo4.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | foo4 - Demo - aratramba.github.io/design-manual/ 7 | 8 | 9 | 10 | 11 | 33 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 |
46 |
4
47 |
5
48 |
6
49 |
50 | 51 | 56 | 57 | 58 | -------------------------------------------------------------------------------- /docs/demos/comments/lib/foo5.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | foo5 - Demo - aratramba.github.io/design-manual/ 7 | 8 | 9 | 10 | 11 | 33 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 |
46 |
7
47 |
48 | 49 | 52 | 53 | 54 | -------------------------------------------------------------------------------- /docs/demos/comments/lib/foo6.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | foo6 - Demo - aratramba.github.io/design-manual/ 7 | 8 | 9 | 10 | 11 | 33 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 |
46 |
1
47 |
2
48 |
3
49 |
4
4b
50 |
5 51 | 52 |
5b
53 |
54 |
55 | 56 | 66 | 67 | 68 | -------------------------------------------------------------------------------- /docs/demos/comments/lib/foo7.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | foo7 - Demo - aratramba.github.io/design-manual/ 7 | 8 | 9 | 10 | 11 | 33 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 |
46 |
1
47 |
2
48 |
3
49 |
4
50 |
5
51 |
6
52 |
53 | 54 | 62 | 63 | 64 | -------------------------------------------------------------------------------- /docs/demos/scrape/components.json: -------------------------------------------------------------------------------- 1 | [ 2 | { 3 | "meta": { 4 | "name": "Navigation", 5 | "description": "This is the navigation component" 6 | }, 7 | "output": "\n
aratramba.github.io/design-manual/
Design Manualv1.0.6
\n
\n\n\n
aratramba.github.io/design-manual/
Design Manualv1.0.6
\n
\n" 8 | }, 9 | { 10 | "meta": { 11 | "name": "Page head", 12 | "description": "This is the page head with logo and button" 13 | }, 14 | "output": "
\n \n
\n

Lightweight Design System Generator

\n
\n Get started ›\n
" 15 | }, 16 | { 17 | "meta": { 18 | "name": "Features", 19 | "description": "Grid with three columns with heading and text" 20 | }, 21 | "output": "
\n
\n
\n

Component library

\n

Mix documentation with real life components to get an active representation of your websites components.

\n
\n
\n

Free form markdown

\n

Write your pages in free form markdown and embed your `!​{components}` wherever you want, as often as you want. No limits.

\n
\n
\n

Fits the way you work

\n

Use your source code documentation, HTML comments or a web scraper to collect components. No need to write markup or code any different.

\n
\n
\n
\n" 22 | }, 23 | { 24 | "meta": { 25 | "name": "Text", 26 | "description": "" 27 | }, 28 | "output": "" 29 | } 30 | ] 31 | -------------------------------------------------------------------------------- /docs/demos/scrape/components.yaml: -------------------------------------------------------------------------------- 1 | - 2 | name: Navigation 3 | description: This is the navigation component 4 | selectors: 5 | - .header 6 | examples: 7 | - | 8 | 13 | {{block}} 14 |
15 | - 16 | name: Page head 17 | description: This is the page head with logo and button 18 | selectors: 19 | - .lead 20 | - 21 | name: Features 22 | description: Grid with three columns with heading and text 23 | selectors: 24 | - .features 25 | examples: 26 | - | 27 |
28 | {{block}} 29 |
30 | - 31 | name: Text 32 | selectors: 33 | - "#-method-3-scrape-your-site" 34 | - "#-method-3-scrape-your-site + p" 35 | examples: 36 | - | 37 |
38 | {{block}} 39 |
-------------------------------------------------------------------------------- /docs/demos/scrape/design-manual-components.json: -------------------------------------------------------------------------------- 1 | [{"meta":{"name":"Navigation","description":"

This is the navigation component

\n"},"output":"\n
aratramba.github.io/design-manual/
Design Manualv1.0.6
\n
\n\n\n
aratramba.github.io/design-manual/
Design Manualv1.0.6
\n
\n","dm":{"libFile":"./lib/navigation.html","height":172}},{"meta":{"name":"Page head","description":"

This is the page head with logo and button

\n"},"output":"
\n \n
\n

Lightweight Design System Generator

\n
\n Get started ›\n
","dm":{"libFile":"./lib/page-head.html","height":446}},{"meta":{"name":"Features","description":"

Grid with three columns with heading and text

\n"},"output":"
\n
\n
\n

Component library

\n

Mix documentation with real life components to get an active representation of your websites components.

\n
\n
\n

Free form markdown

\n

Write your pages in free form markdown and embed your `!​{components}` wherever you want, as often as you want. No limits.

\n
\n
\n

Fits the way you work

\n

Use your source code documentation, HTML comments or a web scraper to collect components. No need to write markup or code any different.

\n
\n
\n
\n","dm":{"libFile":"./lib/features.html","height":194}},{"meta":{"name":"Text","description":""},"output":"","dm":{"libFile":"./lib/text.html","height":20}}] -------------------------------------------------------------------------------- /docs/demos/scrape/index.js: -------------------------------------------------------------------------------- 1 | const DesignManual = require('../../../lib/index'); 2 | const gatherComponents = require('gather-components'); 3 | const rimraf = require('rimraf'); 4 | 5 | gatherComponents({ 6 | url: 'https://aratramba.github.io/design-manual/', 7 | paths: ['index.html', 'getting-started.html'], 8 | components: __dirname + '/components.yaml', 9 | output: __dirname + '/components.json' 10 | }).then(() => { 11 | DesignManual.build({ 12 | output: __dirname, 13 | pages: __dirname, 14 | components: __dirname + '/components.json', 15 | nav: [ 16 | { label: 'Homepage', href: 'index.html' } 17 | ], 18 | meta: { 19 | domain: 'aratramba.github.io/design-manual/', 20 | title: 'Demo' 21 | }, 22 | prerender: { 23 | port: 3000, 24 | path: 'demos/scrape/', 25 | serveFolder: 'docs/' 26 | }, 27 | componentHeadHtml: ` 28 | 29 | 30 | 31 | `, 32 | onComplete: () => { 33 | process.exit(); 34 | } 35 | }); 36 | 37 | rimraf.sync(__dirname + '/design-manual-config.json'); 38 | }); -------------------------------------------------------------------------------- /docs/demos/scrape/index.md: -------------------------------------------------------------------------------- 1 | # Design Manual / Gather Components 2 | This page contains some components scraped from the Design Manual website using [Gather Components](https://www.npmjs.com/package/gather-components). 3 | 4 | --- 5 | 6 | ## Configuration 7 | 8 | * Components are defined in: [components.yaml](https://raw.githubusercontent.com/aratramba/design-manual/master/docs/demos/scrape/components.yaml) 9 | * This page written in markdown: [index.md](https://raw.githubusercontent.com/aratramba/design-manual/master/docs/demos/scrape/index.md) 10 | * The Node.js build script: [index.js](https://raw.githubusercontent.com/aratramba/design-manual/master/docs/demos/scrape/index.js) 11 | 12 | --- 13 | 14 | ## Navigation components 15 | ### Contents 16 | !{Navigation} 17 | !{Page head} 18 | 19 | ## Content components 20 | ### Contents 21 | !{Features} 22 | !{Text} -------------------------------------------------------------------------------- /docs/demos/scrape/lib/features.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | Features - Demo - aratramba.github.io/design-manual/ 7 | 8 | 9 | 10 | 11 | 33 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 |
46 |
47 |
48 |
49 |

Component library

50 |

Mix documentation with real life components to get an active representation of your websites components.

51 |
52 |
53 |

Free form markdown

54 |

Write your pages in free form markdown and embed your `!​{components}` wherever you want, as often as you want. No limits.

55 |
56 |
57 |

Fits the way you work

58 |

Use your source code documentation, HTML comments or a web scraper to collect components. No need to write markup or code any different.

59 |
60 |
61 |
62 | 63 |
64 | 65 | 84 | 85 | 86 | -------------------------------------------------------------------------------- /docs/demos/scrape/lib/navigation.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | Navigation - Demo - aratramba.github.io/design-manual/ 7 | 8 | 9 | 10 | 11 | 33 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 |
46 | 51 |
aratramba.github.io/design-manual/
Design Manualv1.0.6
52 |
53 | 54 | 59 |
aratramba.github.io/design-manual/
Design Manualv1.0.6
60 |
61 | 62 |
63 | 64 | 82 | 83 | 84 | -------------------------------------------------------------------------------- /docs/demos/scrape/lib/page-head.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | Page head - Demo - aratramba.github.io/design-manual/ 7 | 8 | 9 | 10 | 11 | 33 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 |
46 |
47 | 48 |
49 |

Lightweight Design System Generator

50 |
51 | Get started › 52 |
53 |
54 | 55 | 64 | 65 | 66 | -------------------------------------------------------------------------------- /docs/demos/scrape/lib/text.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | Text - Demo - aratramba.github.io/design-manual/ 7 | 8 | 9 | 10 | 11 | 33 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 |
46 | 47 |
48 | 49 | 52 | 53 | 54 | -------------------------------------------------------------------------------- /docs/design-manual-components.json: -------------------------------------------------------------------------------- 1 | [{"meta":{"name":"hello world","description":""},"output":"\"\"","dm":{"libFile":"./lib/hello-world.html","height":70}}] -------------------------------------------------------------------------------- /docs/design-manual-config.json: -------------------------------------------------------------------------------- 1 | {"force":true,"output":"docs/","pages":"docs/src/","components":"docs/components.json","nav":[{"label":"🏡","href":"index.html"},{"label":"Getting started","href":"getting-started.html"},{"label":"Configuration","href":"configuration.html"},{"label":"Examples","href":"examples.html"},{"label":"Github ↗","href":"https://github.com/aratramba/design-manual","target":"_blank"}],"meta":{"domain":"aratramba.github.io/design-manual/","title":"Design Manual","avatar":"./assets/avatar.png","version":"v1.0.6"},"headHtml":"\n \n ","bodyHtml":"","componentBodyHtml":"\n \n ","prerender":{"port":3000,"path":"/","serveFolder":"docs/"}} -------------------------------------------------------------------------------- /docs/examples.html: -------------------------------------------------------------------------------- 1 | Examples - Design Manual - aratramba.github.io/design-manual/ 2 | 3 |
aratramba.github.io/design-manual/
Design Manualv1.0.6

Examples

4 | 8 |

Implementation

9 | 14 |
-------------------------------------------------------------------------------- /docs/lib/hello-world.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | hello world - Design Manual - aratramba.github.io/design-manual/ 7 | 8 | 9 | 10 | 11 | 33 | 38 | 39 | 40 | 41 |
42 | 43 |
44 | 45 | 48 | 49 | 52 | 53 | 54 | -------------------------------------------------------------------------------- /docs/src/Configuration.md: -------------------------------------------------------------------------------- 1 | # Configuration 2 | 3 | ```js 4 | DesignManual.build({ 5 | output: "path/to/export/", 6 | pages: "path/to/pages/", 7 | components: "path/to/components.json", 8 | meta: { 9 | domain: "mywebsite.com", 10 | title: "My Design Manual", 11 | avatar: "http://placehold.it/80x80", 12 | version: "v1.1.0" 13 | }, 14 | nav: [{ label: "Home", href: "/" }], 15 | headHtml: '', 16 | bodyHtml: '', 17 | contentsFlag: "contents", 18 | componentHeadHtml: ` 19 | 20 | 21 | `, 22 | componentBodyHtml: ` 23 | 24 | `, 25 | prerender: { 26 | port: 3000, 27 | path: "/design-manual/", 28 | serveFolder: "/httpdocs/" 29 | }, 30 | onComplete: () => {}, 31 | onLog: msg => {} 32 | }); 33 | ``` 34 | 35 | --- 36 | 37 | ## Options 38 | 39 | | option | default value | type | description | 40 | | --------------------- | ------------- | ------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | 41 | | **output** | null | string | output directory | 42 | | **pages** | null | string | directory that holds your pages | 43 | | **components** | null | string | json file with components | 44 | | **meta** | - | object | | 45 | | **meta.domain** | '' | string | domain for your project | 46 | | **meta.title** | '' | string | title for your project | 47 | | force | false | boolean | force update for all components and pages | 48 | | meta.avatar | '' | string | avatar for your project | 49 | | meta.version | '' | string | version | 50 | | nav | [] | array | array of objects with navigation items | 51 | | - {}.label | - | string | label of the navigation item | 52 | | - {}.href | - | string | link for the navigation item | 53 | | - {}.target | - | string | use \_blank for new window | 54 | | headHtml | '' | string | string of html to include in the head | 55 | | bodyHtml | '' | string | string of html to include in the body | 56 | | componentHeadHtml | '' | string | string of html to include in the head of the component | 57 | | componentBodyHtml | '' | string | string of html to include in the body of the component | 58 | | contentsFlag | 'contents' | string | css id to identify the contents heading | 59 | | renderComponents | true | boolean | turn rendering components on/off | 60 | | renderCSS | true | boolean | turn rendering css on/off | 61 | | prerender | null | object / null | prerender all components to get their heights (at 1200px wide browser window, using Puppeteer). This speeds up the user interface and makes it less jumpy, but makes compiling Design Manual slower because it needs to open all components in a headless browser | 62 | | prerender.port | - | number | static server port for rendering components (http://localhost:{port}) | 63 | | prerender.path | - | string | path to design manual folder (http://localhost:{port}/{path}) | 64 | | prerender.serveFolder | - | string | directory to start the static file server in | 65 | | onComplete | function(){} | function | function to be called when done | 66 | | onLog | console.log | function | custom logging function | 67 | 68 | --- 69 | 70 | ## Custom styling 71 | 72 | You can customize the look and feel by adding an extra css file or a style tag through the `headHtml` option: 73 | 74 | ```js 75 | { 76 | headHtml: ` 77 | 78 | 96 | `; 97 | } 98 | ``` 99 | 100 | All pages get a body class formatted like so: `#{lowercase-slug(filename)}-page`. For example the body class for the file 'My File.md' will be `.my-file-page`. 101 | -------------------------------------------------------------------------------- /docs/src/Examples.md: -------------------------------------------------------------------------------- 1 | # Examples 2 | * [Demo using Gather Components](./demos/scrape) 3 | * [Demo using Collect Components](./demos/comments) 4 | 5 | ## Implementation 6 | * [Design Manual setup with Gulp / Pug-doc](https://gist.github.com/Aratramba/017efabe40e63f02157f974e525f1659) 7 | * [Design Manual setup with Gather Components](https://gist.github.com/Aratramba/896250b0b9695c3900ce29c2a0cf08a8) 8 | * [Design Manual setup with Collect Components](https://gist.github.com/Aratramba/b997f22d75edc8911bd5df6dd1a697d6) 9 | -------------------------------------------------------------------------------- /docs/src/Getting Started.md: -------------------------------------------------------------------------------- 1 | # Getting started 2 | 3 | 1. [Install Design Manual](#install) 4 | 2. [Collect website components](#collect-website-components) 5 | 3. [Setup build](#setup-build) 6 | 4. [Write documentation pages](#write-documentation-pages) 7 | 8 | --- 9 | 10 | ## Install 11 | 12 | First you need to install Design Manual. 13 | 14 | ```bash 15 | > npm i design-manual --save-dev 16 | ``` 17 | 18 | --- 19 | 20 | ## Collect website components 21 | Design Manual doesn't collect the components itself, it just displays them. You need another package to get the components. There are several ways to achieve this. 22 | 23 | * [Documentation inside your source code](#-method-1-documentation-inside-source-code) 24 | * [Use tagged HTML comments](#-method-2-use-html-comments) 25 | * [Scrape your site](#-method-3-scrape-your-site) 26 | 27 | All of these methods output a json file containing HTML-snippets, which Design Manual imports. It's just a matter of where you want your component documentation to live. 28 | 29 | --- 30 | 31 | ### 💎 Method 1: Documentation inside source code 32 | This is the preferred method, for when you have control over the source code that renders the HTML. The source code is where documentation of the components should live and be collected from. Using Pug? Use [Pug-doc](https://www.npmjs.com/package/pug-doc). Using some other renderer? Build you own and let us know. It might be as easy as transforming existing documentation JSON to match ours. As long it outputs a JSON file that looks like the example below you're good to go. 33 | 34 | 35 | ```pug 36 | //- @pugdoc 37 | name: Hello world 38 | 39 | div hello world 40 | ``` 41 | 42 | If you can't use your source code to generate components, tag comments with `@component` and use [method 2, HTML comments](#-method-2-use-html-comments). 43 | 44 | --- 45 | 46 | ### 💬 Method 2: Use HTML comments 47 | If you're not using a static site renderer, or there is not documentation tool you can use, but do have control over the final HTML, use comments to point to the components and scrape them using [Collect Components](https://www.npmjs.com/package/collect-components). 48 | 49 | ```html 50 | 53 | 54 |
hello world
55 | ``` 56 | 57 | --- 58 | 59 | ### 🔪 Method 3: Scrape your site 60 | If you have no control whatsoever over the HTML code, use a scraper based on queryselectors, like [Gather Components](https://www.npmjs.com/package/gather-components). You can use this to build a Design Manual for a website you don't maintain yourself. 61 | 62 | 63 | --- 64 | 65 | ## Setup build 66 | Then set up Design Manual to use this components.json as a source for your markdown files. The most miminal version looks like this: 67 | 68 | ```js 69 | const DesignManual = require('design-manual'); 70 | DesignManual.build({ 71 | output: 'path/to/export/', // destination dir 72 | pages: 'path/to/pages/', // dir containing .md files 73 | components: 'path/to/components.json', // path to the components 74 | meta: { 75 | domain: 'my-domain.com', 76 | title: 'my Design Manual', 77 | version: 'v1.1.0' 78 | } 79 | }); 80 | ``` 81 | 82 | See [configuration](configuration.html) for more information. 83 | 84 | --- 85 | 86 | ## Write documentation pages 87 | Add markdown files for each page you want to create. For example Index.md, Components.md and Guidelines.md 88 | 89 | All `.md` files inside `options.pages` will be used as input. Markdown files in subdirectories will also be rendered. 90 | 91 | A basic page looks something like this: 92 | 93 | ```markdown 94 | # Text page 95 | This is my text page. 96 | 97 | ## Section 1 98 | This is section 1 99 | 100 | ## Section 2 101 | This is section 2 102 | 103 | ### Contents 104 | !{my-component} 105 | ``` 106 | 107 | --- 108 | 109 | ### Embed components 110 | You can embed a component in any page (even multiple times if you wish) by wrapping the components name in `!​{}`. 111 | The tag should be an exact match of a components `meta.name` in your json file. Use double exclamation marks for a simpler view of the component `!!​{}`, with buttons only visible on mouse over and without the description. For a code-first view of the component use `$​{}` or `$$​{}` 112 | 113 | 114 | ```markdown 115 | !{component-name} 116 | ``` 117 | 118 | !{hello world} 119 | !!{hello world} 120 | ${hello world} 121 | $${hello world} 122 | 123 | --- 124 | 125 | ### Table of contents 126 | Use `### Contents` to insert a components table of contents. It will contain all components coming after the contents heading. It will scan for components until it encounters another table of contents heading, or the end of the page. 127 | 128 | If you want to change the text of this heading, edit the `contentsFlag` option when setting up. 129 | 130 | ### Markdown 131 | Markdown is parsed using [marked](https://github.com/chjj/marked). HTML is allowed. 132 | 133 | ### Sidebar 134 | All H2's on the page will be used to create in-page-links in the sidebar navigation. 135 | 136 | --- 137 | 138 | ### Components.json 139 | This is what the json file containing all components should look like. 140 | 141 | ```json 142 | [ 143 | { 144 | "meta": { 145 | "name": "my-component", 146 | "description": "this is my component description" 147 | }, 148 | "output": "
this is some tag
" 149 | } 150 | ] 151 | ``` 152 | 153 | The `meta.description` part is optional and, if present, will be parsed using markdown. 154 | 155 | --- 156 | 157 | ### Autofill 158 | For dev purposes it might be nice to let the pages be filled with all components automatically. 159 | 160 | ```markdown 161 | # This page contains all components 162 | !​{...autofill} 163 | ``` -------------------------------------------------------------------------------- /docs/src/Index.md: -------------------------------------------------------------------------------- 1 |
2 | 3 |
4 |

Lightweight Design System Generator

5 |
6 | Get started › 7 |
8 | 9 | !!{hello world} 10 | 11 |
12 |
13 |

Component library

14 |

Mix documentation with real life components to get an active representation of your websites components.

15 |
16 |
17 |

Free form markdown

18 |

Write your pages in free form markdown and embed your `!​{components}` wherever you want, as often as you want. No limits.

19 |
20 |
21 |

Fits the way you work

22 |

Use your source code documentation, HTML comments or a web scraper to collect components. No need to write markup or code any different.

23 |
24 |
25 | 26 | --- 27 | 28 | ### [NPM Install](https://www.npmjs.com/package/design-manual) 29 | 30 | ```bash 31 | > npm i design-manual collect-components --save-dev 32 | ``` 33 | 34 | ### [Tag your HTML components](./getting-started.html#collect-website-components) 35 | 36 | ```html 37 | 41 | 42 | 43 | ``` 44 | 45 | ### [Embed components in markdown](./getting-started.html#write-documentation-pages) 46 | 47 | ```md 48 | !{hello world} 49 | ``` 50 | 51 | ### [Build](./examples.html#implementation) 52 | 53 | ```js 54 | const DesignManual = require('design-manual'); 55 | const collectComponents = require('collect-components'); 56 | 57 | // gather components 58 | collectComponents({ 59 | url: 'http://localhost:8000/', 60 | paths: ['homepage.html', 'page.html'], 61 | output: 'docs/components.json', 62 | complete: function() { 63 | 64 | // build design manual 65 | DesignManual.build({ 66 | output: 'docs/', 67 | pages: './', 68 | components: 'docs/components.json', 69 | meta: { 70 | domain: 'example.com', 71 | title: 'Example' 72 | } 73 | }); 74 | } 75 | }); 76 | ``` 77 | 78 | ```bash 79 | > Starting design manual 80 | > Starting components 81 | > Found 1 changed component 82 | > Rendering component ./lib/hello-world.html 83 | > Generated components 84 | > Starting pages 85 | > Found 1 changed page 86 | > Generated docs/index.html 87 | > Generated pages 88 | > Design manual complete 89 | ``` 90 | 91 | !{hello world} -------------------------------------------------------------------------------- /docs/src/patterns.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | Patterns 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | -------------------------------------------------------------------------------- /lib/components.js: -------------------------------------------------------------------------------- 1 | const fs = require("fs"); 2 | const path = require("path"); 3 | const slug = require("slug"); 4 | const mkdirp = require("mkdirp"); 5 | const marked = require("marked"); 6 | const rimraf = require("rimraf"); 7 | const cp = require("child_process"); 8 | const killable = require("killable"); 9 | const deepEqual = require("deep-equal"); 10 | const ON_DEATH = require("death")({ uncaughtException: true }); 11 | 12 | let http; 13 | let puppet; 14 | let finalhandler; 15 | let serveStatic; 16 | let server; 17 | let updatedComponents = []; 18 | 19 | const options = JSON.parse(process.argv[2]); 20 | if (!options.renderComponents) { 21 | process.send(1); 22 | return; 23 | } 24 | 25 | const LIB_TEMPLATE = ` 26 | 27 | 28 | 29 | 30 | {{title}} 31 | 32 | 33 | 34 | 35 | 57 | 62 | {{componentHeadHtml}} 63 | 64 | 65 |
66 | {{output}} 67 |
68 | {{componentBodyHtml}} 69 | 72 | 73 | 74 | `; 75 | 76 | /** 77 | * Start 78 | */ 79 | 80 | function start(options) { 81 | console.log("Starting components"); 82 | checkJSON(options); 83 | } 84 | 85 | start(options); 86 | 87 | module.exports.start = start; 88 | 89 | /** 90 | * Check existence of json file 91 | */ 92 | 93 | function checkJSON(options) { 94 | fs.stat(options.components, (err, stats) => { 95 | if (err) { 96 | console.log("Components json not found at" + options.components); 97 | process.send(1); 98 | } 99 | 100 | fs.readFile(path.resolve(options.components), (err, componentsJSON) => { 101 | if (err) { 102 | console.log("Components json not found at" + options.components); 103 | process.send(1); 104 | } 105 | 106 | // shortcut: check if components.json actually 107 | // contains new component information 108 | // compared to design-manual-components.json. 109 | fs.readFile( 110 | path.resolve(options.output, "design-manual-components.json"), 111 | (err, designManualJSON) => { 112 | if (err) { 113 | // no previous components found 114 | } 115 | 116 | try { 117 | designManualJSON = JSON.parse( 118 | (designManualJSON || "[]").toString() 119 | ); 120 | } catch (err) { 121 | designManualJSON = []; 122 | } 123 | 124 | try { 125 | componentsJSON = JSON.parse(componentsJSON.toString()); 126 | } catch (err) { 127 | componentsJSON = []; 128 | } 129 | 130 | let queue = []; 131 | 132 | // loop over components to detect changes 133 | for (let i = 0, l = componentsJSON.length; i < l; ++i) { 134 | // check for meta/name, if not present ignore this component 135 | if ( 136 | !componentsJSON[i].meta || 137 | typeof componentsJSON[i].meta.name === "undefined" 138 | ) { 139 | continue; 140 | } 141 | 142 | // parse description using markdown so it equals the final output 143 | if (componentsJSON[i].meta && componentsJSON[i].meta.description) { 144 | componentsJSON[i].meta.description = marked( 145 | componentsJSON[i].meta.description 146 | ); 147 | } 148 | 149 | // find component match 150 | let matches = designManualJSON.filter(item => { 151 | return item.meta.name === componentsJSON[i].meta.name; 152 | }); 153 | 154 | // no match: new component 155 | if (!matches.length) { 156 | queue.push(componentsJSON[i].meta.name); 157 | 158 | // match found 159 | } else { 160 | // create a clone without the design manual data 161 | let clone = Object.assign({}, matches[0]); 162 | delete clone.dm; 163 | 164 | // match that object with component 165 | if (!deepEqual(componentsJSON[i], clone)) { 166 | queue.push(componentsJSON[i].meta.name); 167 | } 168 | } 169 | } 170 | 171 | componentsJSON = componentsJSON.filter(item => { 172 | return item.meta && item.meta.name; 173 | }); 174 | 175 | if (queue.length === 1) { 176 | console.log("Found 1 changed component"); 177 | } else { 178 | console.log(`Found ${queue.length} changed components`); 179 | } 180 | 181 | if (!queue.length) { 182 | return done(options); 183 | } 184 | 185 | startServer(options, componentsJSON, designManualJSON, queue); 186 | } 187 | ); 188 | }); 189 | }); 190 | } 191 | 192 | /** 193 | * Start server 194 | */ 195 | 196 | function startServer(options, componentsJSON, designManualJSON, queue) { 197 | if (typeof options.prerender === "object" && options.prerender !== null) { 198 | puppet = cp.fork(`${__dirname}/puppeteer.js`); 199 | http = require("http"); 200 | finalhandler = require("finalhandler"); 201 | serveStatic = require("serve-static"); 202 | 203 | puppet.once("message", data => { 204 | if (data === "puppeteer-ready") { 205 | createOutput(options, componentsJSON, designManualJSON, queue); 206 | } 207 | }); 208 | 209 | process.on("exit", quit); 210 | process.on("SIGINT", quit); 211 | 212 | return; 213 | } 214 | 215 | createOutput(options, componentsJSON, designManualJSON, queue); 216 | } 217 | 218 | /** 219 | * Create output dir 220 | */ 221 | 222 | function createOutput(options, componentsJSON, designManualJSON, queue) { 223 | const libPath = path.resolve(options.output, "lib"); 224 | 225 | mkdirp(libPath, err => { 226 | if (err) { 227 | throw err; 228 | } 229 | 230 | render(options, componentsJSON, designManualJSON, queue); 231 | }); 232 | } 233 | 234 | /** 235 | * Render 236 | */ 237 | 238 | function render(options, components, designManualJSON, queue) { 239 | // start static file server to be able to prerender components 240 | if (typeof options.prerender === "object" && options.prerender !== null) { 241 | const serve = serveStatic(path.resolve(options.prerender.serveFolder)); 242 | server = http.createServer((req, res) => { 243 | const done = finalhandler(req, res); 244 | serve(req, res, done); 245 | }); 246 | 247 | server.listen(options.prerender.port); 248 | killable(server); 249 | 250 | server.on("listening", () => { 251 | nextComponent(components, 0); 252 | }); 253 | 254 | server.on("error", () => { 255 | done(options); 256 | }); 257 | } else { 258 | nextComponent(components, 0); 259 | } 260 | 261 | /** 262 | * Process component 263 | */ 264 | 265 | function nextComponent(components, i) { 266 | if (!components.length) { 267 | return completeQueue([], done); 268 | } 269 | 270 | // copy old data when its a direct match 271 | if (queue.indexOf(components[i].meta.name) === -1) { 272 | let match = designManualJSON.filter(item => { 273 | return components[i].meta.name === item.meta.name; 274 | }); 275 | 276 | if (match.length) { 277 | components[i] = match[0]; 278 | } 279 | 280 | return proceed(components, i); 281 | } 282 | 283 | // add design manual namespace 284 | components[i].dm = {}; 285 | 286 | // generate component filee 287 | components[i].dm.libFile = generateLibFile(components[i]); 288 | 289 | // get height of component using puppeteer 290 | renderComponent(components[i].dm.libFile, result => { 291 | components[i].dm.height = result.height; 292 | return proceed(components, i); 293 | }); 294 | } 295 | 296 | /** 297 | * Proceed 298 | */ 299 | 300 | function proceed(components, i) { 301 | if (i < components.length - 1) { 302 | nextComponent(components, i + 1); 303 | } else { 304 | return completeQueue(components, done); 305 | } 306 | } 307 | 308 | /** 309 | * Complete 310 | */ 311 | 312 | function completeQueue(data, cb) { 313 | fs.writeFile( 314 | path.resolve(options.output, "design-manual-components.json"), 315 | JSON.stringify(data), 316 | () => { 317 | cb(options, designManualJSON); 318 | } 319 | ); 320 | } 321 | } 322 | 323 | /** 324 | * Get lib file path 325 | */ 326 | 327 | function getLibFilePath(name) { 328 | const id = slug(name, { lower: true }); 329 | const libFilePath = path.resolve(options.output, "lib", id + ".html"); 330 | return libFilePath; 331 | } 332 | 333 | /** 334 | * Generate lib file 335 | */ 336 | 337 | function generateLibFile(component) { 338 | const libFilePath = getLibFilePath(component.meta.name); 339 | const file = ["./lib/", path.basename(libFilePath)].join(""); 340 | 341 | console.log("Rendering component " + file); 342 | 343 | updatedComponents.push(component.meta.name); 344 | 345 | const html = LIB_TEMPLATE.replace("{{output}}", component.output) 346 | .replace("{{raw_output}}", component.output) 347 | .replace("{{componentHeadHtml}}", options.componentHeadHtml) 348 | .replace("{{componentBodyHtml}}", options.componentBodyHtml) 349 | .replace( 350 | "{{title}}", 351 | [component.meta.name, options.meta.title, options.meta.domain].join(" - ") 352 | ); 353 | 354 | fs.writeFile(libFilePath, html, err => { 355 | if (err) throw err; 356 | }); 357 | 358 | return file; 359 | } 360 | 361 | /** 362 | * Get component height 363 | */ 364 | 365 | function renderComponent(file, cb) { 366 | if (typeof options.prerender !== "object" || options.prerender === null) { 367 | return cb({ height: null }); 368 | } 369 | 370 | if (puppet) { 371 | puppet.once("message", data => { 372 | if (data.height) { 373 | cb({ 374 | height: data.height 375 | }); 376 | } 377 | }); 378 | 379 | puppet.send({ 380 | url: 381 | "http://localhost:" + 382 | options.prerender.port + 383 | "/" + 384 | options.prerender.path + 385 | file 386 | }); 387 | } 388 | } 389 | 390 | /** 391 | * Quit 392 | */ 393 | 394 | function quit() { 395 | if (puppet) { 396 | puppet.kill("SIGINT"); 397 | puppet = null; 398 | } 399 | 400 | if (server) { 401 | server.kill(); 402 | } 403 | 404 | process.removeListener("exit", quit); 405 | process.removeListener("SIGINT", quit); 406 | } 407 | 408 | /** 409 | * Done 410 | */ 411 | 412 | function done(options) { 413 | // clean up unused files 414 | fs.readdir(path.resolve(options.output, "lib"), (err, files) => { 415 | fs.readFile( 416 | path.resolve(options.output, "design-manual-components.json"), 417 | (err, data) => { 418 | const designManualJSON = JSON.parse(data); 419 | 420 | if (files) { 421 | for (let i = 0, l = files.length; i < l; ++i) { 422 | const file = files[i]; 423 | 424 | const match = designManualJSON.filter(item => { 425 | return path.basename(item.dm.libFile) === file; 426 | }); 427 | 428 | if (!match.length) { 429 | rimraf.sync(path.resolve(options.output, "lib", file), { 430 | read: false 431 | }); 432 | } 433 | } 434 | } 435 | 436 | quit(); 437 | process.send(updatedComponents); 438 | process.exit(); 439 | } 440 | ); 441 | }); 442 | } 443 | 444 | ON_DEATH((signal, err) => { 445 | process.exit(); 446 | }); 447 | -------------------------------------------------------------------------------- /lib/constants.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | COMPONENT_EMBED_REGEX: /([!]{1,2}|[$]{1,2}){(.*)}/g, 3 | COMPONENT_AUTOFILL_REGEX: /([!]{1,2}|[$]{1,2}){(\.\.\.autofill)}/g, 4 | COMPONENT_ID_REGEX: /(?<={).+?(?=})/g, 5 | COMPONENT_TYPE_PREVIEW: "preview", 6 | COMPONENT_TYPE_CODE: "code", 7 | H2_REGEX: /^#{2}(?!#)(.*)/gm, 8 | COMPONENT_OPTION_FRAMELESS: "frameless" 9 | }; 10 | -------------------------------------------------------------------------------- /lib/css.js: -------------------------------------------------------------------------------- 1 | const fs = require("fs"); 2 | const mkdirp = require("mkdirp"); 3 | const sass = require("node-sass"); 4 | const postcss = require("postcss"); 5 | const CleanCSS = require("clean-css"); 6 | const autoprefixer = require("autoprefixer"); 7 | const ON_DEATH = require("death")({ uncaughtException: true }); 8 | 9 | const options = JSON.parse(process.argv[2]); 10 | 11 | if (!options.renderCSS) { 12 | process.send(1); 13 | return; 14 | } 15 | 16 | /** 17 | * Generate css 18 | */ 19 | 20 | mkdirp.sync(options.output); 21 | 22 | sass.render( 23 | { 24 | file: __dirname + "/../template/styles/all.scss", 25 | outFile: options.output + "/all.min.css" 26 | }, 27 | (err, result) => { 28 | if (err) { 29 | console.log(err); 30 | process.exitCode = 1; 31 | return; 32 | } 33 | 34 | postcss([autoprefixer]) 35 | .process(result.css, { from: false }) 36 | .then(result => { 37 | const cleaned = new CleanCSS().minify(result.css); 38 | fs.writeFile(options.output + "/all.min.css", cleaned.styles, err => { 39 | if (err) { 40 | throw err; 41 | } 42 | 43 | process.send(1); 44 | }); 45 | }); 46 | } 47 | ); 48 | 49 | ON_DEATH((signal, err) => { 50 | process.exit(); 51 | }); 52 | -------------------------------------------------------------------------------- /lib/index.js: -------------------------------------------------------------------------------- 1 | const cp = require("child_process"); 2 | const path = require("path"); 3 | const fs = require("fs"); 4 | const mkdirp = require("mkdirp"); 5 | const deepEqual = require("deep-equal"); 6 | 7 | let renderer = { 8 | css: null, 9 | components: null, 10 | pages: null 11 | }; 12 | 13 | let queue; 14 | 15 | /** 16 | * Build 17 | */ 18 | 19 | function build(config) { 20 | if (queue && queue.length) { 21 | interrupt(config); 22 | return; 23 | } 24 | 25 | queue = []; 26 | 27 | // options 28 | let options = Object.assign( 29 | { 30 | force: false, 31 | output: null, 32 | meta: { 33 | domain: "", 34 | title: "", 35 | avatar: "", 36 | version: "" 37 | }, 38 | nav: [], 39 | headHtml: "", 40 | bodyHtml: "", 41 | componentHeadHtml: "", 42 | componentBodyHtml: "", 43 | contentsFlag: "contents", 44 | renderComponents: true, 45 | renderCSS: true, 46 | prerender: null, 47 | onLog: console.log, 48 | onComplete: () => {} 49 | }, 50 | config 51 | ); 52 | 53 | if (!options.components) { 54 | log("Design Manual error: options.components is required", options.onLog); 55 | return complete(null, options); 56 | } 57 | 58 | if (!options.pages) { 59 | log("Design Manual error: options.pages is required", options.onLog); 60 | return complete(null, options); 61 | } 62 | 63 | if (!options.output) { 64 | log("Design Manual error: options.output is required", options.onLog); 65 | return complete(null, options); 66 | } 67 | 68 | if (!options.meta) { 69 | log("Design Manual error: options.meta is required", options.onLog); 70 | return complete(null, options); 71 | } 72 | 73 | if (!options.meta.title) { 74 | log("Design Manual error: options.meta.title is required", options.onLog); 75 | return complete(null, options); 76 | } 77 | 78 | if (!options.meta.domain) { 79 | log("Design Manual error: options.meta.domain is required", options.onLog); 80 | return complete(null, options); 81 | } 82 | 83 | log("Starting design manual", options.onLog); 84 | 85 | // force page/components rebuild 86 | if (options.force) { 87 | options.forceComponents = true; 88 | options.forcePages = true; 89 | 90 | // check current component configuration against previous config 91 | // if componentBodyHtml or componentHeadHtml is changed, force component rebuild 92 | } else { 93 | try { 94 | let prevConfig = fs.readFileSync( 95 | path.resolve(options.output, "design-manual-config.json"), 96 | "utf8" 97 | ); 98 | if (prevConfig) { 99 | try { 100 | prevConfig = JSON.parse(prevConfig.toString()); 101 | 102 | const changedConfigMessage = "Detected changed configuration"; 103 | 104 | // force components when component html changed 105 | if ( 106 | options.componentHeadHtml !== (prevConfig.componentHeadHtml || "") 107 | ) { 108 | log( 109 | `${changedConfigMessage} (components head html)`, 110 | options.onLog 111 | ); 112 | options.forceComponents = true; 113 | } 114 | 115 | // force components when component html changed 116 | if ( 117 | options.componentBodyHtml !== (prevConfig.componentBodyHtml || "") 118 | ) { 119 | log( 120 | `${changedConfigMessage} (components body html)`, 121 | options.onLog 122 | ); 123 | options.forceComponents = true; 124 | } 125 | 126 | // force pages when meta changed 127 | if ( 128 | !deepEqual( 129 | options.meta, 130 | prevConfig.meta || { 131 | domain: "", 132 | title: "", 133 | avatar: "", 134 | version: "" 135 | } 136 | ) 137 | ) { 138 | log(`${changedConfigMessage} (meta)`, options.onLog); 139 | options.forcePages = true; 140 | } 141 | 142 | // force pages when nav changed 143 | if (!deepEqual(options.nav, prevConfig.nav || [])) { 144 | log(`${changedConfigMessage} (nav)`, options.onLog); 145 | options.forcePages = true; 146 | } 147 | 148 | // force pages when page head html changed 149 | if (options.headHtml !== (prevConfig.headHtml || "")) { 150 | log(`${changedConfigMessage} (head html)`, options.onLog); 151 | options.forcePages = true; 152 | } 153 | 154 | // force pages when page body html changed 155 | if (options.bodyHtml !== (prevConfig.bodyHtml || "")) { 156 | log(`${changedConfigMessage} (body html)`, options.onLog); 157 | options.forcePages = true; 158 | } 159 | 160 | // force pages when contentsflag changed 161 | if ( 162 | options.contentsFlag !== (prevConfig.contentsFlag || "contents") 163 | ) { 164 | log(`${changedConfigMessage} (contents flag)`, options.onLog); 165 | options.forcePages = true; 166 | } 167 | 168 | // force pages when prerender changed 169 | if (!deepEqual(options.prerender, prevConfig.prerender)) { 170 | log(`${changedConfigMessage} (prerender)`, options.onLog); 171 | options.forcePages = true; 172 | } 173 | 174 | // force both when output has been changed 175 | if (options.output !== (prevConfig.output || null)) { 176 | // unreachable: when output changes, there will be no previous 177 | // configuration, so it will never come here 178 | } 179 | } catch (err) { 180 | log("Error parsing previous configuration", options.onLog); 181 | options.forceComponents = true; 182 | options.forcePages = true; 183 | } 184 | } 185 | } catch (err) { 186 | log("Could not find previous configuration", options.onLog); 187 | options.forceComponents = true; 188 | options.forcePages = true; 189 | } 190 | } 191 | 192 | // store current config 193 | mkdirp.sync(options.output); 194 | fs.writeFileSync( 195 | path.resolve(options.output, "design-manual-config.json"), 196 | JSON.stringify(config) 197 | ); 198 | 199 | // resolve ouput paths 200 | options.output = path.resolve(options.output); 201 | options.pages = path.resolve(options.pages); 202 | options.components = path.resolve(options.components); 203 | 204 | // force rebuild by writing empty components file 205 | if (options.forceComponents) { 206 | log("Forcing components rebuild", options.onLog); 207 | fs.writeFileSync( 208 | path.resolve(options.output, "design-manual-components.json"), 209 | "[]" 210 | ); 211 | } 212 | 213 | if (options.forcePages) { 214 | log("Forcing pages rebuild", options.onLog); 215 | } 216 | 217 | // create queue 218 | queue = ["components"]; 219 | if (options.renderCSS) queue.push("css"); 220 | if (options.renderComponents) queue.push("components"); 221 | 222 | // render 223 | if (options.renderCSS) render("css", options); 224 | 225 | // generate components 226 | if (options.renderComponents) { 227 | render("components", options, updatedComponents => { 228 | options.updatedComponents = updatedComponents; 229 | render("pages", options); 230 | }); 231 | } else { 232 | render("pages", options); 233 | } 234 | } 235 | 236 | module.exports.build = build; 237 | 238 | /** 239 | * Render 240 | */ 241 | 242 | function render(type, options, cb) { 243 | renderer[type] = cp.fork( 244 | `${__dirname}/${type}.js`, 245 | [[JSON.stringify(options)]], 246 | { silent: true } 247 | ); 248 | renderer[type].on("message", data => { 249 | if (cb) cb(data); 250 | complete(type, options); 251 | }); 252 | 253 | renderer[type].stderr.on("data", data => { 254 | console.error(`\n${data}`); 255 | }); 256 | 257 | renderer[type].stdout.on("data", data => { 258 | log(data, options.onLog); 259 | }); 260 | } 261 | 262 | /** 263 | * Complete 264 | */ 265 | 266 | function complete(type, options) { 267 | if (type !== null) { 268 | log("Generated " + type, options.onLog); 269 | queue.splice(queue.indexOf(type), 1); 270 | } 271 | 272 | if (queue.length === 0) { 273 | log("Design manual complete", options.onLog); 274 | 275 | if (typeof options.onComplete === "function") { 276 | options.onComplete(); 277 | } 278 | } 279 | } 280 | 281 | /** 282 | * Interrupt 283 | */ 284 | 285 | function interrupt(config) { 286 | log("Caught interrupt signal, closing", config.onLog); 287 | 288 | if (queue.indexOf("css") > -1 && renderer["css"]) { 289 | renderer["css"].kill(); 290 | } 291 | if (queue.indexOf("components") > -1 && renderer["components"]) { 292 | renderer["components"].kill(); 293 | } 294 | if (queue.indexOf("pages") > -1 && renderer["pages"]) { 295 | renderer["pages"].kill(); 296 | } 297 | 298 | queue = null; 299 | build(config); 300 | } 301 | 302 | module.exports.interrupt = interrupt; 303 | 304 | /** 305 | * Log 306 | */ 307 | 308 | function log(msg, onLog) { 309 | if (typeof onLog === "function") onLog(msg.toString().replace(/\n/g, "")); 310 | } 311 | -------------------------------------------------------------------------------- /lib/pages.js: -------------------------------------------------------------------------------- 1 | const ON_DEATH = require("death")({ uncaughtException: true }); 2 | const recursive = require("recursive-readdir"); 3 | const marked = require("marked"); 4 | const mkdirp = require("mkdirp"); 5 | const rimraf = require("rimraf"); 6 | const async = require("async"); 7 | const path = require("path"); 8 | const slug = require("slug"); 9 | const pug = require("pug"); 10 | const fs = require("fs"); 11 | 12 | const utils = require("./utils"); 13 | const analyzer = require("./static-analysis"); 14 | const constants = require("./constants"); 15 | 16 | const NO_HEADINGS_CLASS = "has-no-headings"; 17 | 18 | const COMPONENT_TEMPLATE = __dirname + "/../template/templates/_component.pug"; 19 | const TABLE_OF_CONTENTS_TEMPLATE = 20 | __dirname + "/../template/templates/_table-of-contents.pug"; 21 | const PAGE_TEMPLATE = __dirname + "/../template/templates/base.pug"; 22 | 23 | const options = JSON.parse(process.argv[2]); 24 | const updatedComponents = options.updatedComponents || []; 25 | 26 | /** 27 | * Start 28 | */ 29 | 30 | console.log("Starting pages"); 31 | 32 | // read components json file 33 | let componentsJSON; 34 | if (options.renderComponents) { 35 | try { 36 | componentsJSON = JSON.parse( 37 | fs 38 | .readFileSync( 39 | path.resolve(options.output, "design-manual-components.json") 40 | ) 41 | .toString() 42 | ); 43 | } catch (err) { 44 | console.log( 45 | `Design Manual json not found at ${options.components}. Please try again.` 46 | ); 47 | process.send(0); 48 | } 49 | } 50 | 51 | /** 52 | * Check if a Markdown file is newer 53 | * than the html file. Ignore if it isnt. 54 | */ 55 | 56 | function ignore(mdFile, stats) { 57 | // if force is on, don't ignore 58 | if (options.forcePages) { 59 | return false; 60 | } 61 | 62 | // if dir doesn't exist yet, don't ignore 63 | if (stats.isDirectory()) { 64 | return false; 65 | } 66 | 67 | const output = utils.getOutputInfo(mdFile, options.pages, options.output); 68 | 69 | // if no html file exists, don't ignore 70 | if (!fs.existsSync(output.htmlFile)) { 71 | return false; 72 | } 73 | 74 | // compare dates of md file and html file 75 | // if md file is newer, don't ignore 76 | const htmlStat = fs.statSync(output.htmlFile); 77 | if (new Date(stats.mtime) > new Date(htmlStat.mtime)) { 78 | return false; 79 | } 80 | 81 | // read the markdown file 82 | const mdFileData = fs.readFileSync( 83 | path.resolve(options.pages, mdFile), 84 | "utf8" 85 | ); 86 | 87 | // if it contains the ...autofill component 88 | if ( 89 | mdFileData.indexOf(`!{...autofill}`) > -1 || 90 | mdFileData.indexOf(`\${...autofill}`) > -1 91 | ) { 92 | return false; 93 | } 94 | 95 | // read file 96 | // if it contains a component, don't ignore 97 | if (updatedComponents.length) { 98 | // if it contains any of the updated components 99 | for (let i = 0, l = updatedComponents.length; i < l; ++i) { 100 | if ( 101 | mdFileData.indexOf(`!{${updatedComponents[i]}}`) > -1 || 102 | mdFileData.indexOf(`\${${updatedComponents[i]}}`) > -1 103 | ) { 104 | return false; 105 | } 106 | } 107 | } 108 | 109 | // default, ignore file 110 | return true; 111 | } 112 | 113 | /** 114 | * Loop over queue and start processing 115 | */ 116 | 117 | recursive(options.pages, ["!*.md", ignore], (err, files) => { 118 | if (files.length === 1) { 119 | console.log("Found 1 changed page"); 120 | } else { 121 | console.log(`Found ${files.length} changed pages`); 122 | } 123 | 124 | if (!files.length) { 125 | return done(); 126 | } 127 | 128 | async.map( 129 | files, 130 | (file, next) => { 131 | processFile(file, componentsJSON, next); 132 | }, 133 | () => { 134 | done(); 135 | } 136 | ); 137 | }); 138 | 139 | /** 140 | * Clean up unused html files 141 | */ 142 | 143 | recursive(options.pages, (err, mdFiles) => { 144 | let htmlFiles = []; 145 | 146 | for (let i = 0, l = mdFiles.length; i < l; ++i) { 147 | const mdFile = mdFiles[i]; 148 | const output = utils.getOutputInfo(mdFile, options.pages, options.output); 149 | htmlFiles.push(output.htmlFile); 150 | } 151 | 152 | recursive( 153 | options.output, 154 | [options.pages, "!*.html", path.join(options.output, "/lib/**/*.html")], 155 | (err, files) => { 156 | for (let i = 0, l = files.length; i < l; ++i) { 157 | if (htmlFiles.indexOf(files[i]) === -1) { 158 | rimraf.sync(files[i], { read: false }); 159 | } 160 | } 161 | } 162 | ); 163 | }); 164 | 165 | /** 166 | * Process file 167 | */ 168 | 169 | function processFile(file, componentsJSON, next) { 170 | let fileStream = fs.createReadStream(path.resolve(options.pages, file)); 171 | fileStream.on("data", async data => { 172 | try { 173 | // get file info 174 | const output = utils.getOutputInfo(file, options.pages, options.output); 175 | 176 | const directoryDepth = path.relative(options.pages, file).split("/") 177 | .length; 178 | const directoryDepthPath = new Array(directoryDepth).join("../"); 179 | 180 | data = data.toString(); 181 | 182 | // if it contains the ...autofill component 183 | if (data.match(constants.COMPONENT_AUTOFILL_REGEX)) { 184 | // do a scan 185 | const staticScan = await analyzer.scan(options.pages); 186 | const unusedComponents = analyzer.getScanUnusedComponents( 187 | staticScan, 188 | componentsJSON 189 | ); 190 | 191 | // replace !{...autofill} with string of unused component embeds !{component1}\n!{component2} 192 | data = data.replace(constants.COMPONENT_AUTOFILL_REGEX, (match, p1) => { 193 | const str = [...unusedComponents] 194 | .map(component => { 195 | return `${p1}{${component}}`; 196 | }) 197 | .join("\n"); 198 | return str; 199 | }); 200 | } 201 | 202 | /** 203 | * setup markdown lexer 204 | */ 205 | 206 | const lexer = new marked.Lexer({ 207 | gfm: true, 208 | tables: true, 209 | breaks: true 210 | }); 211 | 212 | const tokens = lexer.lex(data.toString()); 213 | 214 | /** 215 | * Scan markdown tokens 216 | * to get a list of headings, table of contents and components 217 | */ 218 | 219 | const headings = []; 220 | const tablesOfContents = []; 221 | const pageComponents = []; 222 | const pageComponentsRegister = {}; 223 | 224 | for (let i = 0, l = tokens.length; i < l; i++) { 225 | /** 226 | * Scan for headings 227 | * Store heading with token index 228 | */ 229 | 230 | if (tokens[i].type === "heading" && tokens[i].depth === 2) { 231 | headings.push({ 232 | label: tokens[i].text, 233 | slug: slug(tokens[i].text, { lower: true }) 234 | }); 235 | } 236 | 237 | /** 238 | * Scan for table of contents 239 | * Store table of contents with token index 240 | */ 241 | 242 | if ( 243 | tokens[i].type === "heading" && 244 | tokens[i].text.toLowerCase() === options.contentsFlag.toLowerCase() 245 | ) { 246 | tablesOfContents.push({ 247 | index: i, 248 | components: [] 249 | }); 250 | } 251 | 252 | /** 253 | * Scan for components 254 | * Store component with token index 255 | */ 256 | 257 | if ( 258 | tokens[i].type === "paragraph" && 259 | constants.COMPONENT_EMBED_REGEX.test(tokens[i].text.trim()) 260 | ) { 261 | const matches = tokens[i].text 262 | .trim() 263 | .match(constants.COMPONENT_EMBED_REGEX); 264 | 265 | for (let ii = 0, ll = matches.length; ii < ll; ++ii) { 266 | let componentName = matches[ii].match(/{(.*)}/)[1]; 267 | let componentSlug = slug(componentName, { lower: true }); 268 | 269 | // add to list of components 270 | pageComponents.push({ 271 | index: i, 272 | tag: componentSlug 273 | }); 274 | 275 | // add component to current table of contents 276 | if (tablesOfContents.length) { 277 | tablesOfContents[tablesOfContents.length - 1].components.push({ 278 | name: componentName, 279 | slug: componentSlug, 280 | hasError: !(componentsJSON || []).filter(item => { 281 | if (item.meta.name === componentName) { 282 | return item; 283 | } 284 | }).length 285 | }); 286 | } 287 | } 288 | } 289 | } 290 | 291 | /** 292 | * Render tables of contents 293 | */ 294 | 295 | for (let i = 0, l = tablesOfContents.length; i < l; ++i) { 296 | let tableHTML = pug.renderFile(TABLE_OF_CONTENTS_TEMPLATE, { 297 | components: tablesOfContents[i].components 298 | }); 299 | 300 | tokens[tablesOfContents[i].index].type = "html"; 301 | tokens[tablesOfContents[i].index].text = tableHTML; 302 | } 303 | 304 | /** 305 | * Render embed codes for components 306 | * paragraphs like this !{component} or !!{component} 307 | * will become component html with an iframe and html source 308 | */ 309 | 310 | for (let i = 0, l = pageComponents.length; i < l; ++i) { 311 | // change type from paragraph to html 312 | tokens[pageComponents[i].index].type = "html"; 313 | 314 | // update text to rendered html component 315 | tokens[pageComponents[i].index].text = tokens[ 316 | pageComponents[i].index 317 | ].text.replace( 318 | constants.COMPONENT_EMBED_REGEX, 319 | (match, prefix, componentName) => { 320 | // set component slug 321 | let componentSlug = slug(componentName, { lower: true }); 322 | 323 | // make sure slug unique by adding counter 324 | if (pageComponentsRegister[componentSlug]) { 325 | pageComponentsRegister[componentSlug] += 1; 326 | componentSlug = 327 | componentSlug + "-" + pageComponentsRegister[componentSlug]; 328 | } else { 329 | pageComponentsRegister[componentSlug] = 1; 330 | } 331 | 332 | // find component inside componentsJSON 333 | let componentData = (componentsJSON || []).filter(item => { 334 | if (item.meta.name === componentName) { 335 | return item; 336 | } 337 | })[0]; 338 | 339 | let componentHTML; 340 | 341 | // add component embed type 342 | let embedOptions = []; 343 | 344 | if (match.substr(0, 1) === "$") { 345 | embedOptions.push(constants.COMPONENT_TYPE_CODE); 346 | } 347 | 348 | if (match.substr(0, 2) === "!!" || match.substr(0, 2) === "$$") { 349 | embedOptions.push(constants.COMPONENT_OPTION_FRAMELESS); 350 | } 351 | 352 | // if component was found in json file 353 | if (componentData) { 354 | // render component 355 | componentHTML = pug.renderFile(COMPONENT_TEMPLATE, { 356 | component: { 357 | slug: componentSlug, 358 | file: componentData.file, 359 | embedOptions: embedOptions, 360 | meta: { 361 | name: componentData.meta.name, 362 | description: componentData.meta.description 363 | }, 364 | height: componentData.dm.height, 365 | libFile: directoryDepthPath + componentData.dm.libFile 366 | } 367 | }); 368 | } else { 369 | componentHTML = pug.renderFile(COMPONENT_TEMPLATE, { 370 | component: { 371 | slug: componentName, 372 | embedOptions: embedOptions 373 | } 374 | }); 375 | } 376 | 377 | return componentHTML; 378 | } 379 | ); 380 | } 381 | 382 | /** 383 | * Render the page with altered content 384 | */ 385 | 386 | const contentHTML = marked.parser(tokens); 387 | 388 | /** 389 | * Set body classes 390 | */ 391 | 392 | let bodyClass = []; 393 | 394 | // add slugified filename to page body class 395 | bodyClass.push(slug(utils.getPageName(file), { lower: true }) + "-page"); 396 | 397 | // add body class if no headings are found 398 | if (!headings.length) { 399 | bodyClass.push(NO_HEADINGS_CLASS); 400 | } 401 | 402 | /** 403 | * Generate page 404 | */ 405 | 406 | mkdirp.sync(path.join(options.output, output.fileDir)); 407 | 408 | let currentPage = path.join(output.fileDir, output.fileName); 409 | 410 | const locals = Object.assign({ 411 | content: contentHTML, 412 | navItems: options.nav, 413 | pageTitle: utils.getPageTitle(output.htmlFile, options.meta), 414 | currentPage: currentPage, 415 | meta: options.meta, 416 | headHtml: options.headHtml, 417 | bodyHtml: options.bodyHtml, 418 | headings: headings, 419 | bodyClass: bodyClass.join(" "), 420 | rootPath: 421 | path.relative(output.htmlFile, options.output).substring(1) + "/" 422 | }); 423 | 424 | const pageHTML = pug.renderFile(PAGE_TEMPLATE, locals); 425 | 426 | let outputStream = fs.createWriteStream(output.htmlFile); 427 | 428 | outputStream.write(pageHTML); 429 | outputStream.end(); 430 | console.log("Generated " + path.relative(process.cwd(), output.htmlFile)); 431 | next(); 432 | } catch (err) { 433 | console.log(err); 434 | } 435 | }); 436 | } 437 | 438 | module.exports.processFile = processFile; 439 | 440 | /** 441 | * Done 442 | */ 443 | 444 | function done() { 445 | process.send(1); 446 | } 447 | 448 | ON_DEATH(() => { 449 | process.exit(); 450 | }); 451 | -------------------------------------------------------------------------------- /lib/puppeteer.js: -------------------------------------------------------------------------------- 1 | /** 2 | * This gets the height of the component 3 | * and returns that to the main process 4 | */ 5 | 6 | const puppeteer = require("puppeteer"); 7 | 8 | (async () => { 9 | const browser = await puppeteer.launch(); 10 | const page = await browser.newPage(); 11 | 12 | /** 13 | * Dismiss any dialogs 14 | */ 15 | 16 | page.on("dialog", async dialog => { 17 | await dialog.dismiss(); 18 | }); 19 | 20 | /** 21 | * Get page height 22 | */ 23 | 24 | async function getHeight(url) { 25 | await page.goto(url); 26 | 27 | const height = await page.evaluate(() => { 28 | return document.body.getBoundingClientRect().height; 29 | }); 30 | 31 | return height; 32 | } 33 | 34 | /** 35 | * Notify process app is ready 36 | */ 37 | 38 | process.send("puppeteer-ready"); 39 | 40 | /** 41 | * Receive message 42 | */ 43 | 44 | process.on("message", data => { 45 | if (data) { 46 | if (data.exit) { 47 | browser.close(); 48 | process.exitCode = 1; 49 | return; 50 | } else if (data.url) { 51 | getHeight(data.url).then(height => { 52 | process.send({ 53 | height: height 54 | }); 55 | }); 56 | } 57 | } 58 | }); 59 | })(); 60 | -------------------------------------------------------------------------------- /lib/static-analysis.js: -------------------------------------------------------------------------------- 1 | const recursive = require("recursive-readdir"); 2 | const path = require("path"); 3 | const fs = require("fs"); 4 | const constants = require("./constants"); 5 | 6 | /** 7 | * Scan all markdown files to check for components 8 | * returns component defintion 9 | * 10 | * !{component} 11 | * ${component} 12 | * !!{component} 13 | * $${component} 14 | * !{...autofill} 15 | * 16 | * returns an array with objects like: 17 | * 18 | * [ 19 | * { 20 | * file: 'file.md', 21 | * headings: ['heading 1', 'heading 2'], 22 | * components: [ 23 | * type: 'preview', // preview|code 24 | * frameless: false, 25 | * tag: '!{component1}', 26 | * id: 'component1', 27 | * specials: [''] // only autofill is a special for now 28 | * ] 29 | * } 30 | * ] 31 | */ 32 | 33 | function scan(dir) { 34 | return new Promise(resolve => { 35 | let results = []; 36 | 37 | recursive(dir, ["!*.md"], (err, files) => { 38 | for (let i = 0, l = files.length; i < l; ++i) { 39 | const mdFileData = fs.readFileSync(files[i], "utf8"); 40 | const pageComponents = scanPageComponents(mdFileData, files[i]); 41 | const headings = scanPageHeadings(mdFileData, files[i]); 42 | 43 | results.push({ 44 | file: path.relative(process.cwd(), files[i]), 45 | headings: headings, 46 | components: pageComponents 47 | }); 48 | } 49 | resolve(results); 50 | }); 51 | }); 52 | } 53 | 54 | module.exports.scan = scan; 55 | 56 | /** 57 | * Scan for components 58 | */ 59 | 60 | function scanPageComponents(mdFileData) { 61 | const matches = mdFileData.match(constants.COMPONENT_EMBED_REGEX); 62 | const components = []; 63 | 64 | if (matches) { 65 | for (let ii = 0, ll = matches.length; ii < ll; ++ii) { 66 | const match = matches[ii]; 67 | const id = match.match(constants.COMPONENT_ID_REGEX)[0]; 68 | 69 | // view option 70 | let type; 71 | if (match.substr(0, 1) === "$") { 72 | type = constants.COMPONENT_TYPE_CODE; 73 | } else { 74 | type = constants.COMPONENT_TYPE_PREVIEW; 75 | } 76 | 77 | // frameless option 78 | let frameless; 79 | if (match.substr(0, 2) === "!!" || match.substr(0, 2) === "$$") { 80 | frameless = true; 81 | } else { 82 | frameless = false; 83 | } 84 | 85 | // specials: autofill 86 | let specials = []; 87 | if (match.indexOf("{...autofill}") > -1) { 88 | specials.push("autofill"); 89 | } 90 | 91 | // return analysis 92 | components.push({ 93 | type: type, 94 | frameless: frameless, 95 | tag: match, 96 | id: id, 97 | specials: specials 98 | }); 99 | } 100 | } 101 | 102 | return components; 103 | } 104 | 105 | module.exports.scanPageComponents = scanPageComponents; 106 | 107 | /** 108 | * Scan for H2's 109 | */ 110 | 111 | function scanPageHeadings(mdFileData) { 112 | const headings = []; 113 | const matches = mdFileData.match(constants.H2_REGEX); 114 | 115 | if (matches) { 116 | for (let ii = 0, ll = matches.length; ii < ll; ++ii) { 117 | const match = matches[ii].split("##")[1].trim(); 118 | headings.push(match); 119 | } 120 | } 121 | return headings; 122 | } 123 | 124 | module.exports.scanPageHeadings = scanPageHeadings; 125 | 126 | /** 127 | * Return all used components in a set 128 | */ 129 | 130 | function getScanUsedComponents(scan) { 131 | function flatten(arr) { 132 | return Array.prototype.concat(...arr); 133 | } 134 | 135 | return new Set( 136 | flatten( 137 | scan.map(file => { 138 | return file.components.map(component => { 139 | return component.id; 140 | }); 141 | }) 142 | ) 143 | ); 144 | } 145 | 146 | module.exports.getScanUsedComponents = getScanUsedComponents; 147 | 148 | /** 149 | * Reduce all components to a set of unused components 150 | */ 151 | 152 | function getScanUnusedComponents(scan, allComponents) { 153 | const usedComponents = getScanUsedComponents(scan); 154 | const unusedComponents = allComponents.filter(component => { 155 | return !usedComponents.has(component.meta.name); 156 | }); 157 | 158 | return new Set( 159 | unusedComponents.map(component => { 160 | return component.meta.name; 161 | }) 162 | ); 163 | } 164 | 165 | module.exports.getScanUnusedComponents = getScanUnusedComponents; 166 | 167 | /** 168 | * Report for directory and list of components 169 | */ 170 | 171 | function report(dir, components) { 172 | console.log(`Design Manual report for ${dir}\n`); 173 | 174 | scan(dir).then(results => { 175 | if (results.length) { 176 | console.log(`${results.length} pages`); 177 | console.log( 178 | results.map(result => { 179 | return result.file; 180 | }) 181 | ); 182 | console.log(); 183 | } 184 | 185 | const usedComponents = getScanUsedComponents(results); 186 | const unusedComponents = getScanUnusedComponents(results, components); 187 | 188 | console.log(`found ${usedComponents.size} used components`); 189 | 190 | if (usedComponents.size) { 191 | console.log([...usedComponents]); 192 | console.log(); 193 | } 194 | 195 | console.log(`found ${unusedComponents.size} unused components`); 196 | if (unusedComponents.size) { 197 | console.log([...unusedComponents]); 198 | console.log(); 199 | } 200 | }); 201 | } 202 | 203 | module.exports.report = report; 204 | -------------------------------------------------------------------------------- /lib/utils.js: -------------------------------------------------------------------------------- 1 | const toSentenceCase = require("to-sentence-case"); 2 | const path = require("path"); 3 | const slug = require("slug"); 4 | 5 | /** 6 | * Get Page name, used in navigation and for file name 7 | */ 8 | 9 | function getPageName(filename) { 10 | return path.basename(filename).split(".md")[0]; 11 | } 12 | 13 | module.exports.getPageName = getPageName; 14 | 15 | /** 16 | * Get page title, used inside 17 | */ 18 | 19 | function getPageTitle(filename, meta) { 20 | return [ 21 | toSentenceCase(path.basename(filename).split(".html")[0]), 22 | meta.title, 23 | meta.domain 24 | ].join(" - "); 25 | } 26 | 27 | module.exports.getPageTitle = getPageTitle; 28 | 29 | /** 30 | * Get output file 31 | */ 32 | 33 | function getOutputFile(filename) { 34 | return slug(getPageName(filename), { lower: true }) + ".html"; 35 | } 36 | 37 | module.exports.getOutputFile = getOutputFile; 38 | 39 | /** 40 | * Get output info 41 | */ 42 | 43 | function getOutputInfo(file, pagesDir, outputDir) { 44 | var fileName = getOutputFile(file); 45 | var fileDir = path.dirname(path.relative(pagesDir, file)); 46 | var htmlFile = path.join(outputDir, fileDir, fileName); 47 | 48 | return { 49 | fileName: fileName, 50 | fileDir: fileDir, 51 | htmlFile: htmlFile 52 | }; 53 | } 54 | 55 | module.exports.getOutputInfo = getOutputInfo; 56 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "design-manual", 3 | "version": "1.0.7", 4 | "description": "Lightweight Design System Generator", 5 | "main": "lib/index.js", 6 | "directories": { 7 | "test": "test" 8 | }, 9 | "scripts": { 10 | "precommit": "pretty-quick --staged", 11 | "prepublishOnly": "node docs/build.js -q", 12 | "template-js-browserify": "browserify template/scripts/index -t uglifyify", 13 | "template-css-sass": "node-sass template/styles/all.scss | cleancss", 14 | "template-js": "npm run template-js-browserify -- -o httpdocs/app.min.js", 15 | "template-css": "npm run template-css-sass -- -o httpdocs/all.min.css", 16 | "template-html": "jade template/templates/components.pug -o httpdocs", 17 | "watch-js": "watchify template/scripts/index.js -o examples/httpdocs/app.min.js", 18 | "docs": "node docs/build.js -q", 19 | "test": "ava test/**/*/test*.js --serial --verbose", 20 | "test-config": "ava test/config/*.js --serial", 21 | "test-validate": "ava test/config/test-validate.js --serial", 22 | "test-defaults": "ava test/defaults/test.js --serial", 23 | "test-gulp": "ava test/gulp/test.js --serial", 24 | "test-interrupt": "ava test/interrupt/test.js --serial", 25 | "test-prerender": "ava test/prerender/test.js --serial", 26 | "test-puppeteer": "ava test/puppeteer/test.js --serial", 27 | "test-render-css": "ava test/render-css/test.js --serial", 28 | "test-render-pages": "ava test/render-pages/test.js --serial", 29 | "test-autofill": "ava test/autofill/test.js --serial" 30 | }, 31 | "author": "Eight Media", 32 | "license": "ISC", 33 | "devDependencies": { 34 | "ava": "^3.15.0", 35 | "collect-components": "^1.2.2", 36 | "eslint": "^7.18.0", 37 | "eslint-config-airbnb": "^18.2.1", 38 | "eslint-config-airbnb-base": "^14.2.1", 39 | "eslint-plugin-import": "^2.22.1", 40 | "gather-components": "^0.2.0", 41 | "gulp": "^4.0.2", 42 | "is-there": "^4.5.1", 43 | "rimraf": "^3.0.2" 44 | }, 45 | "dependencies": { 46 | "async": "^3.2.0", 47 | "autoprefixer": "^10.2.3", 48 | "clean-css": "^4.2.3", 49 | "cssmin": "^0.4.3", 50 | "death": "^1.1.0", 51 | "deep-equal": "^2.0.5", 52 | "delegate-events": "^1.1.1", 53 | "events": "^3.2.0", 54 | "finalhandler": "^1.1.2", 55 | "html": "^1.0.0", 56 | "husky": "^4.3.8", 57 | "interactjs": "^1.10.2", 58 | "killable": "^1.0.1", 59 | "marked": "^1.2.8", 60 | "minimist": "^1.2.5", 61 | "mkdirp": "^0.5.1", 62 | "node-sass": "^5.0.0", 63 | "postcss": "^8.2.4", 64 | "prettier": "^2.2.1", 65 | "pretty-quick": "^3.1.0", 66 | "pug": "^3.0.0", 67 | "puppeteer": "^5.5.0", 68 | "recursive-readdir": "^2.2.2", 69 | "serve-static": "^1.14.1", 70 | "slug": "^4.0.2", 71 | "to-sentence-case": "^1.0.0", 72 | "vanilla-lazyload": "^17.3.0" 73 | }, 74 | "repository": { 75 | "type": "git", 76 | "url": "git+https://github.com/aratramba/design-manual.git" 77 | }, 78 | "keywords": [ 79 | "styleguide", 80 | "design system", 81 | "component library", 82 | "pattern library" 83 | ], 84 | "bugs": { 85 | "url": "https://github.com/aratramba/design-manual/issues" 86 | }, 87 | "homepage": "https://github.com/aratramba/design-manual#readme" 88 | } 89 | -------------------------------------------------------------------------------- /template/styles/_vars.scss: -------------------------------------------------------------------------------- 1 | $sidebar-width: 200px; 2 | 3 | $grey: #ddd; 4 | $text: #666; 5 | $bg-grey: #F4F5F7; 6 | 7 | $primary: #2A66DC; 8 | $primary-contrast: WHITESMOKE; 9 | 10 | $breakpoint-s: 500px; 11 | $breakpoint-m: 750px; 12 | $breakpoint-l: 990px; 13 | $breakpoint-xl: 1300px; -------------------------------------------------------------------------------- /template/styles/all.scss: -------------------------------------------------------------------------------- 1 | @import 'vars'; 2 | 3 | @import 'base/reset'; 4 | @import 'base/base'; 5 | 6 | @import 'components/content'; 7 | @import 'components/sidebar'; 8 | @import 'components/header'; 9 | @import 'components/code'; 10 | @import 'components/breadcrumbs'; 11 | @import 'components/scrolltop'; 12 | @import 'components/table-of-contents'; 13 | @import 'components/component'; -------------------------------------------------------------------------------- /template/styles/base/_base.scss: -------------------------------------------------------------------------------- 1 | *, *::before, *::after { 2 | box-sizing: border-box; 3 | } 4 | 5 | html, body { 6 | margin: 0; 7 | padding: 0; 8 | font-family: -apple-system,BlinkMacSystemFont,'Segoe UI',Roboto,Helvetica,Arial,sans-serif,'Apple Color Emoji','Segoe UI Emoji','Segoe UI Symbol'; 9 | font-size: 16px; 10 | font-weight: 400; 11 | overflow-x: hidden; 12 | color: #4D4542; 13 | -webkit-tap-highlight-color: rgba(0,0,0,0); 14 | -webkit-font-smoothing: antialiased; 15 | } 16 | 17 | img { 18 | max-width: 100%; 19 | } 20 | 21 | 22 | /** 23 | * Main content wrapper 24 | */ 25 | 26 | .main { 27 | padding: 0; 28 | margin-left: auto; 29 | margin-right: auto; 30 | position: relative; 31 | width: 100%; 32 | max-width: 1160px; 33 | padding-left: 20px; 34 | padding-right: 20px; 35 | margin-top: 95px; 36 | margin-bottom: 95px; 37 | 38 | @media (min-width: $breakpoint-xl) { 39 | margin-left: $sidebar-width; 40 | max-width: calc(100% - #{$sidebar-width}); 41 | } 42 | 43 | .has-no-headings & { 44 | @media (min-width: $breakpoint-xl) { 45 | margin-left: 0; 46 | max-width: 100%; 47 | } 48 | } 49 | } -------------------------------------------------------------------------------- /template/styles/base/_reset.scss: -------------------------------------------------------------------------------- 1 | /* http://meyerweb.com/eric/tools/css/reset/ 2 | v2.0 | 20110126 3 | License: none (public domain) 4 | */ 5 | 6 | html, body, div, span, applet, object, iframe, 7 | h1, h2, h3, h4, h5, h6, p, blockquote, pre, 8 | a, abbr, acronym, address, big, cite, code, 9 | del, dfn, em, img, ins, kbd, q, s, samp, 10 | small, strike, strong, sub, sup, tt, var, 11 | b, u, i, center, 12 | dl, dt, dd, ol, ul, li, 13 | fieldset, form, label, legend, 14 | table, caption, tbody, tfoot, thead, tr, th, td, 15 | article, aside, canvas, details, embed, 16 | figure, figcaption, footer, header, hgroup, 17 | menu, nav, output, ruby, section, summary, 18 | time, mark, audio, video { 19 | margin: 0; 20 | padding: 0; 21 | border: 0; 22 | font-size: 100%; 23 | font: inherit; 24 | vertical-align: baseline; 25 | } 26 | /* HTML5 display-role reset for older browsers */ 27 | article, aside, details, figcaption, figure, 28 | footer, header, hgroup, menu, nav, section { 29 | display: block; 30 | } 31 | body { 32 | line-height: 1; 33 | } 34 | ol, ul { 35 | list-style: none; 36 | } 37 | blockquote, q { 38 | quotes: none; 39 | } 40 | blockquote::before, blockquote::after, 41 | q::before, q::after { 42 | content: ''; 43 | content: none; 44 | } 45 | table { 46 | border-collapse: collapse; 47 | border-spacing: 0; 48 | } -------------------------------------------------------------------------------- /template/styles/components/_breadcrumbs.scss: -------------------------------------------------------------------------------- 1 | .breadcrumbs { 2 | color: $primary-contrast; 3 | padding-top: 25px; 4 | padding-left: 25px; 5 | font-size: 14px; 6 | font-weight: 600; 7 | opacity: 0; 8 | transition: opacity .15s; 9 | position: absolute; 10 | left: 50px; 11 | white-space: nowrap; 12 | 13 | @media (min-width: $breakpoint-m) { 14 | padding: 22px 20px; 15 | font-size: 16px; 16 | } 17 | 18 | @media (min-width: $breakpoint-xl) { 19 | left: 0; 20 | } 21 | } 22 | 23 | .breadcrumbs__item { 24 | display: inline-block; 25 | } 26 | 27 | .breadcrumbs__item--current { 28 | &::after { 29 | opacity: .5; 30 | margin: 0 7px; 31 | content: ''; 32 | width: 7px; 33 | height: 12px; 34 | vertical-align: middle; 35 | display: inline-block; 36 | background: no-repeat url('data:image/svg+xml;utf8,') 37 | } 38 | } 39 | 40 | .breadcrumbs__section { 41 | color: $primary-contrast; 42 | 43 | &:hover{ 44 | text-decoration: underline; 45 | } 46 | } 47 | 48 | 49 | .is-scrolled-in-section { 50 | .breadcrumbs { 51 | opacity: 1; 52 | } 53 | 54 | .breadcrumbs__seperator { 55 | display: inline-block; 56 | } 57 | } -------------------------------------------------------------------------------- /template/styles/components/_code.scss: -------------------------------------------------------------------------------- 1 | /* PrismJS 1.14.0 2 | http://prismjs.com/download.html#themes=prism-twilight&languages=markup+css+clike+javascript+diff+json+markdown+scss */ 3 | /** 4 | * prism.js Twilight theme 5 | * Based (more or less) on the Twilight theme originally of Textmate fame. 6 | * @author Remy Bach 7 | */ 8 | code[class*="language-"], 9 | pre[class*="language-"] { 10 | color: white; 11 | background: none; 12 | font-family: Consolas, Monaco, 'Andale Mono', 'Ubuntu Mono', monospace; 13 | text-align: left; 14 | text-shadow: 0 -.1em .2em black; 15 | white-space: pre; 16 | word-spacing: normal; 17 | word-break: normal; 18 | word-wrap: normal; 19 | line-height: 1.5; 20 | 21 | -moz-tab-size: 4; 22 | -o-tab-size: 4; 23 | tab-size: 4; 24 | 25 | -webkit-hyphens: none; 26 | -moz-hyphens: none; 27 | -ms-hyphens: none; 28 | hyphens: none; 29 | } 30 | 31 | pre[class*="language-"], 32 | :not(pre) > code[class*="language-"] { 33 | background: hsl(0, 0%, 8%); /* #141414 */ 34 | } 35 | 36 | /* Code blocks */ 37 | pre[class*="language-"] { 38 | border-radius: .5em; 39 | border: .3em solid hsl(0, 0%, 33%); /* #282A2B */ 40 | box-shadow: 1px 1px .5em black inset; 41 | margin: .5em 0; 42 | overflow: auto; 43 | padding: 1em; 44 | } 45 | 46 | pre[class*="language-"]::-moz-selection { 47 | /* Firefox */ 48 | background: hsl(200, 4%, 16%); /* #282A2B */ 49 | } 50 | 51 | pre[class*="language-"]::selection { 52 | /* Safari */ 53 | background: hsl(200, 4%, 16%); /* #282A2B */ 54 | } 55 | 56 | /* Text Selection colour */ 57 | pre[class*="language-"]::-moz-selection, pre[class*="language-"] ::-moz-selection, 58 | code[class*="language-"]::-moz-selection, code[class*="language-"] ::-moz-selection { 59 | text-shadow: none; 60 | background: hsla(0, 0%, 93%, 0.15); /* #EDEDED */ 61 | } 62 | 63 | pre[class*="language-"]::selection, pre[class*="language-"] ::selection, 64 | code[class*="language-"]::selection, code[class*="language-"] ::selection { 65 | text-shadow: none; 66 | background: hsla(0, 0%, 93%, 0.15); /* #EDEDED */ 67 | } 68 | 69 | /* Inline code */ 70 | :not(pre) > code[class*="language-"] { 71 | border-radius: .3em; 72 | border: .13em solid hsl(0, 0%, 33%); /* #545454 */ 73 | box-shadow: 1px 1px .3em -.1em black inset; 74 | padding: .15em .2em .05em; 75 | white-space: normal; 76 | } 77 | 78 | .token.comment, 79 | .token.prolog, 80 | .token.doctype, 81 | .token.cdata { 82 | color: hsl(0, 0%, 47%); /* #777777 */ 83 | } 84 | 85 | .token.punctuation { 86 | opacity: .7; 87 | } 88 | 89 | .namespace { 90 | opacity: .7; 91 | } 92 | 93 | .token.tag, 94 | .token.boolean, 95 | .token.number, 96 | .token.deleted { 97 | color: hsl(14, 58%, 55%); /* #CF6A4C */ 98 | } 99 | 100 | .token.keyword, 101 | .token.property, 102 | .token.selector, 103 | .token.constant, 104 | .token.symbol, 105 | .token.builtin { 106 | color: hsl(53, 89%, 79%); /* #F9EE98 */ 107 | } 108 | 109 | .token.attr-name, 110 | .token.attr-value, 111 | .token.string, 112 | .token.char, 113 | .token.operator, 114 | .token.entity, 115 | .token.url, 116 | .language-css .token.string, 117 | .style .token.string, 118 | .token.variable, 119 | .token.inserted { 120 | color: hsl(76, 21%, 52%); /* #8F9D6A */ 121 | } 122 | 123 | .token.atrule { 124 | color: hsl(218, 22%, 55%); /* #7587A6 */ 125 | } 126 | 127 | .token.regex, 128 | .token.important { 129 | color: hsl(42, 75%, 65%); /* #E9C062 */ 130 | } 131 | 132 | .token.important, 133 | .token.bold { 134 | font-weight: bold; 135 | } 136 | .token.italic { 137 | font-style: italic; 138 | } 139 | 140 | .token.entity { 141 | cursor: help; 142 | } 143 | 144 | pre[data-line] { 145 | padding: 1em 0 1em 3em; 146 | position: relative; 147 | } 148 | 149 | /* Markup */ 150 | .language-markup .token.tag, 151 | .language-markup .token.attr-name, 152 | .language-markup .token.punctuation { 153 | color: hsl(33, 33%, 52%); /* #AC885B */ 154 | } 155 | 156 | /* Make the tokens sit above the line highlight so the colours don't look faded. */ 157 | .token { 158 | position: relative; 159 | z-index: 1; 160 | } 161 | 162 | .line-highlight { 163 | background: hsla(0, 0%, 33%, 0.25); /* #545454 */ 164 | background: linear-gradient(to right, hsla(0, 0%, 33%, .1) 70%, hsla(0, 0%, 33%, 0)); /* #545454 */ 165 | border-bottom: 1px dashed hsl(0, 0%, 33%); /* #545454 */ 166 | border-top: 1px dashed hsl(0, 0%, 33%); /* #545454 */ 167 | left: 0; 168 | line-height: inherit; 169 | margin-top: 0.75em; /* Same as .prism’s padding-top */ 170 | padding: inherit 0; 171 | pointer-events: none; 172 | position: absolute; 173 | right: 0; 174 | white-space: pre; 175 | z-index: 0; 176 | } 177 | 178 | .line-highlight:before, 179 | .line-highlight[data-end]:after { 180 | background-color: hsl(215, 15%, 59%); /* #8794A6 */ 181 | border-radius: 999px; 182 | box-shadow: 0 1px white; 183 | color: hsl(24, 20%, 95%); /* #F5F2F0 */ 184 | content: attr(data-start); 185 | font: bold 65%/1.5 sans-serif; 186 | left: .6em; 187 | min-width: 1em; 188 | padding: 0 .5em; 189 | position: absolute; 190 | text-align: center; 191 | text-shadow: none; 192 | top: .4em; 193 | vertical-align: .3em; 194 | } 195 | 196 | .line-highlight[data-end]:after { 197 | bottom: .4em; 198 | content: attr(data-end); 199 | top: auto; 200 | } 201 | 202 | -------------------------------------------------------------------------------- /template/styles/components/_component.scss: -------------------------------------------------------------------------------- 1 | @mixin code-button { 2 | border: 1px solid $grey; 3 | border-radius: 3px; 4 | padding: 6px 8px; 5 | margin-left: 5px; 6 | display: inline-block; 7 | transition: background-color .15s; 8 | } 9 | 10 | 11 | /** 12 | * Displays a component 13 | * 14 | * Nested this inside `.component` 15 | * to compensate for `.content h1` styling (see _content.scss). 16 | */ 17 | 18 | @keyframes pace-spinner { 19 | 0% { 20 | transform: rotate(0deg); 21 | transform: rotate(0deg); 22 | } 23 | 100% { 24 | transform: rotate(360deg); 25 | transform: rotate(360deg); 26 | } 27 | } 28 | 29 | .component { 30 | padding-bottom: 50px; 31 | margin-top: 50px; 32 | 33 | &.is-frameless{ 34 | margin-top: 24px; 35 | } 36 | } 37 | 38 | .component__title { 39 | margin-bottom: 5px; 40 | } 41 | 42 | .component__description { 43 | margin-bottom: 1em; 44 | margin-bottom: 5px; 45 | } 46 | 47 | .component__404 { 48 | background-color: lightyellow; 49 | border: 1px solid $grey; 50 | margin: 0 0 -150px; 51 | padding: 10px; 52 | display: inline-block; 53 | border-radius: 3px; 54 | color: #333; 55 | } 56 | 57 | 58 | /** 59 | * Meta (buttons) 60 | */ 61 | 62 | .component__meta { 63 | transition: opacity .15s; 64 | 65 | .is-frameless & { 66 | opacity: 0; 67 | pointer-events: none; 68 | } 69 | 70 | .is-frameless:hover & { 71 | opacity: 1; 72 | pointer-events: auto; 73 | } 74 | } 75 | 76 | .component__meta__text { 77 | .is-frameless & { 78 | display: none; 79 | } 80 | } 81 | 82 | .component__meta__text__description { 83 | .is-frameless & { 84 | display: none; 85 | } 86 | } 87 | 88 | .component__meta__buttons { 89 | border-top: 1px solid $grey; 90 | text-align: right; 91 | font-size: 11px; 92 | margin-top: 12px; 93 | margin-bottom: 12px; 94 | user-select: none; 95 | 96 | .is-frameless & { 97 | border: 0; 98 | margin-top: 0; 99 | } 100 | 101 | &:before { 102 | content: ''; 103 | position: relative; 104 | top: -5px; 105 | width: 30px; 106 | height: 5px; 107 | background-color: $primary; 108 | display: block; 109 | 110 | .is-frameless & { 111 | display: none; 112 | } 113 | } 114 | } 115 | 116 | .component__meta__buttons__title { 117 | display: none; 118 | 119 | .is-frameless & { 120 | display: inline-block; 121 | } 122 | } 123 | 124 | .component__meta__buttons__code { 125 | @include code-button; 126 | cursor: pointer; 127 | opacity: 1; 128 | transition: opacity .15s; 129 | 130 | .is-loading & { 131 | pointer-events: none; 132 | opacity: 0; 133 | } 134 | 135 | &:hover { 136 | background-color: $grey; 137 | } 138 | 139 | &::before { 140 | content: ''; 141 | width: 12px; 142 | height: 9px; 143 | margin-right: 5px; 144 | display: inline-block; 145 | vertical-align: middle; 146 | background: url(data:image/svg+xml;base64,PHN2ZyB3aWR0aD0iMTIiIGhlaWdodD0iOSIgdmlld0JveD0iMCAwIDEyIDkiIHhtbG5zPSJodHRwOi8vd3d3LnczLm9yZy8yMDAwL3N2ZyI+PHBhdGggZD0iTTMuNTU0IDYuOTkzbC0uMzEuMzFjLS4wNDIuMDQyLS4wOS4wNjMtLjE0NC4wNjMtLjA1MyAwLS4xLS4wMi0uMTQyLS4wNjJMLjA2MiA0LjQxQy4wMjIgNC4zNjYgMCA0LjMyIDAgNC4yNjVjMC0uMDU0LjAyLS4xMDIuMDYyLS4xNDNsMi44OTYtMi44OTZjLjA0LS4wNC4wOS0uMDYyLjE0Mi0uMDYyLjA1NCAwIC4xMDIuMDIuMTQzLjA2MmwuMzEuMzFjLjA0Mi4wNDIuMDYzLjA5LjA2My4xNDQgMCAuMDU1LS4wMi4xMDItLjA2Mi4xNDRMMS4xMTIgNC4yNjZsMi40NDIgMi40NGMuMDQuMDQzLjA2Mi4wOS4wNjIuMTQ0IDAgLjA1NC0uMDIuMTAyLS4wNjIuMTQzek03LjIyNi4zNjNMNC45MSA4LjM4NmMtLjAxOC4wNTQtLjA1LjA5NC0uMDk4LjEyLS4wNDcuMDI4LS4wOTYuMDMzLS4xNDYuMDE3bC0uMzg1LS4xMDZjLS4wNTMtLjAxNi0uMDkzLS4wNS0uMTItLjA5Ni0uMDI3LS4wNDgtLjAzMi0uMS0uMDE2LS4xNTJMNi40NjIuMTQ2Yy4wMTYtLjA1NC4wNS0uMDk0LjA5Ni0uMTIuMDQ4LS4wMjguMDk2LS4wMzMuMTQ2LS4wMTdsLjM4NS4xMDVjLjA1My4wMTcuMDk0LjA1LjEyLjA5Ni4wMjcuMDUuMDMzLjEuMDE2LjE1NHptNC4wODIgNC4wNDZMOC40MTMgNy4zMDNjLS4wNDIuMDQtLjA5LjA2Mi0uMTQzLjA2Mi0uMDU0IDAtLjEwMi0uMDItLjE0My0uMDYybC0uMzEtLjMxYy0uMDQyLS4wNDItLjA2My0uMDktLjA2My0uMTQ0IDAtLjA1NC4wMi0uMS4wNjItLjE0M2wyLjQ0Mi0yLjQ0LTIuNDQyLTIuNDQzYy0uMDQtLjA0Mi0uMDYyLS4wOS0uMDYyLS4xNDMgMC0uMDUzLjAyLS4xLjA2Mi0uMTQybC4zMS0uMzFjLjA0Mi0uMDQyLjA5LS4wNjMuMTQ0LS4wNjMuMDU0IDAgLjEuMDIuMTQzLjA2MmwyLjg5NSAyLjg5NmMuMDQyLjA0LjA2Mi4wOS4wNjIuMTQzIDAgLjA1My0uMDIuMS0uMDYyLjE0M3oiIGZpbGw9IiM2NjYiIGZpbGwtcnVsZT0iZXZlbm9kZCIvPjwvc3ZnPg==) no-repeat top center; 147 | } 148 | } 149 | 150 | .component__meta__buttons__link { 151 | @include code-button; 152 | display: inline-block; 153 | vertical-align: top; 154 | padding-top: 4px; 155 | padding-bottom: 4px; 156 | 157 | svg { 158 | fill: red; 159 | width: 16px; 160 | height: 16px; 161 | display: block; 162 | fill: $grey; 163 | opacity: .4; 164 | transition: opacity .15s; 165 | } 166 | 167 | &:hover { 168 | svg { 169 | opacity: 1; 170 | } 171 | } 172 | } 173 | .component__caption { 174 | display: inline-block; 175 | margin-top: 10px; 176 | font-size: 12px; 177 | } 178 | 179 | 180 | /** 181 | * Code snippets 182 | */ 183 | 184 | .component__code { 185 | display: none; 186 | width: 100%; 187 | border: 1px solid $grey; 188 | 189 | pre[class*="language-"] { 190 | border: 0; 191 | border-radius: 0; 192 | margin: 0; 193 | } 194 | } 195 | 196 | /** 197 | * Iframe that holds the component 198 | */ 199 | 200 | .component__preview { 201 | position: relative; 202 | max-width: 100%; 203 | min-width: 275px; 204 | padding-right: 24px; 205 | border: 1px solid $grey; 206 | 207 | &.is-resizing { 208 | .component__preview__handle { 209 | background-color: #f5f5f5; 210 | border-color: $grey; 211 | 212 | svg { 213 | opacity: .5; 214 | } 215 | } 216 | } 217 | 218 | .is-loading & { 219 | &::before { 220 | content: ''; 221 | display: block; 222 | position: absolute; 223 | top: 12px; 224 | left: 12px; 225 | width: 14px; 226 | height: 14px; 227 | border: solid 2px transparent; 228 | border-top-color: $primary; 229 | border-left-color: $primary; 230 | border-radius: 10px; 231 | animation: pace-spinner 400ms linear infinite; 232 | } 233 | } 234 | 235 | .has-error & { 236 | &::before { 237 | content: '!'; 238 | display: block; 239 | position: absolute; 240 | top: 12px; 241 | left: 12px; 242 | color: white; 243 | width: 20px; 244 | height: 20px; 245 | text-align: center; 246 | line-height: 14px; 247 | font-size: 13px; 248 | font-weight: 600; 249 | border-radius: 50%; 250 | padding: 3px; 251 | background-color: crimson; 252 | } 253 | } 254 | } 255 | 256 | .component__preview__handle { 257 | display: block; 258 | position: absolute; 259 | width: 24px; 260 | height: 100%; 261 | right: 0; 262 | top: 0; 263 | bottom: 0; 264 | transition: background-color .15s, border-color .15s; 265 | border-left: 1px solid transparent; 266 | 267 | &:hover { 268 | background-color: #f5f5f5; 269 | border-color: $grey; 270 | 271 | svg { 272 | opacity: .5; 273 | } 274 | } 275 | 276 | svg { 277 | display: block; 278 | width: 6px; 279 | height: 23px; 280 | position: absolute; 281 | top: 50%; 282 | left: 50%; 283 | transform: translate(-50%, -50%); 284 | opacity: .25; 285 | transition: opacity .15s; 286 | } 287 | } 288 | 289 | .component__preview__iframe { 290 | width: 100%; 291 | opacity: 0; 292 | transition: opacity .5s; 293 | 294 | &.loaded { 295 | opacity: 1; 296 | } 297 | } 298 | 299 | 300 | /** 301 | * Toggle 302 | */ 303 | 304 | .component__toggle { 305 | display: none; 306 | } 307 | 308 | .component__toggle:checked ~ .component__code { 309 | display: block; 310 | } 311 | 312 | .component__toggle:checked ~ .component__preview { 313 | display: none; 314 | } 315 | 316 | .component__toggle:checked ~ .component__meta .component__meta__toggle { 317 | background-color: $grey; 318 | } -------------------------------------------------------------------------------- /template/styles/components/_content.scss: -------------------------------------------------------------------------------- 1 | .content { 2 | 3 | h1 { 4 | line-height: 1.2; 5 | font-size: 40px; 6 | font-weight: 600; 7 | color: $primary; 8 | margin-bottom: 20px; 9 | max-width: 740px; 10 | } 11 | 12 | h2 { 13 | line-height: 1.2; 14 | font-size: 20px; 15 | font-weight: 600; 16 | margin-bottom: 10px; 17 | max-width: 740px; 18 | } 19 | 20 | h3, 21 | h4, 22 | h5, 23 | h6 { 24 | font-weight: 600; 25 | line-height: 1.2; 26 | margin-bottom: 17px; 27 | font-size: 16px; 28 | margin-bottom: 10px; 29 | max-width: 740px; 30 | } 31 | 32 | p, 33 | li { 34 | line-height: 1.7; 35 | margin-bottom: 1em; 36 | font-weight: 400; 37 | font-size: 15px; 38 | max-width: 740px; 39 | } 40 | 41 | ol, 42 | ul { 43 | list-style: disc; 44 | margin-left: 1em; 45 | margin-bottom: 1em; 46 | 47 | ol, 48 | ul { 49 | margin-bottom: 0; 50 | } 51 | } 52 | 53 | ol { 54 | list-style: decimal; 55 | } 56 | 57 | li { 58 | margin-bottom: 0; 59 | } 60 | 61 | a { 62 | color: #4183C4; 63 | text-decoration: underline; 64 | 65 | &:hover { 66 | text-decoration: none; 67 | } 68 | } 69 | 70 | strong { 71 | font-weight: bold; 72 | } 73 | 74 | 75 | /** 76 | * Table styling 77 | */ 78 | 79 | table { 80 | width: 100%; 81 | border: 1px dotted $grey; 82 | border-bottom: 0; 83 | margin-bottom: 1em; 84 | 85 | caption { 86 | padding: .7em 15px; 87 | text-align: left; 88 | font-size: 1.3em; 89 | margin: auto -3px; 90 | border-radius: 3px; 91 | } 92 | 93 | th { 94 | font-weight: bold; 95 | background-color: $bg-grey; 96 | border-right: 1px dotted $grey; 97 | 98 | &:last-of-type { 99 | border-right: 0; 100 | } 101 | } 102 | 103 | td { 104 | background: white; 105 | border-bottom: 1px dotted $grey; 106 | border-right: 1px dotted $grey; 107 | 108 | &:last-of-type { 109 | border-right: 0; 110 | } 111 | } 112 | 113 | td, 114 | th { 115 | padding: .7em 15px; 116 | text-align: left; 117 | line-height: 1.7; 118 | font-weight: 400; 119 | font-size: 15px; 120 | 121 | &:empty { 122 | &::after { 123 | content: ''; 124 | display: block; 125 | height: 1em; 126 | } 127 | } 128 | } 129 | 130 | thead { 131 | td, 132 | th { 133 | font-weight: bold; 134 | } 135 | } 136 | 137 | tbody { 138 | td, 139 | th { 140 | vertical-align: top; 141 | } 142 | } 143 | } 144 | 145 | 146 | hr { 147 | border: 0; 148 | background-color: $grey; 149 | height: 1px; 150 | margin: 50px 0; 151 | } 152 | 153 | h1 code, 154 | h2 code, 155 | h3 code, 156 | h4 code, 157 | h5 code, 158 | h6 code, 159 | td code, 160 | th code, 161 | li code, 162 | p code { 163 | padding: 0 3px; 164 | margin: 0; 165 | font-size: .85em; 166 | background-color: rgba(27, 31, 35, .05); 167 | border-radius: 3px; 168 | font-family: monospace; 169 | display: inline-block; 170 | } 171 | 172 | code[class*="language-"], 173 | pre[class*="language-"] { 174 | font-size: 13.6px; 175 | line-height: 19px; 176 | background-color: #1B1D23; 177 | box-shadow: none; 178 | } 179 | 180 | pre[class*="language-"] { 181 | border: 1px solid #eee; 182 | border-radius: 0; 183 | margin-bottom: 24px; 184 | } 185 | } -------------------------------------------------------------------------------- /template/styles/components/_header.scss: -------------------------------------------------------------------------------- 1 | .header { 2 | position: fixed; 3 | height: 60px; 4 | left: 0; 5 | right: 0; 6 | top: 0; 7 | background-color: $primary; 8 | z-index: 100; 9 | border-bottom: 1px solid rgba(black, .1); 10 | overflow: hidden; 11 | 12 | @media (min-width: $breakpoint-xl) { 13 | left: $sidebar-width; 14 | } 15 | } 16 | 17 | .has-no-headings .header { 18 | @media (min-width: $breakpoint-xl) { 19 | left: 0; 20 | } 21 | } 22 | 23 | .has-no-headings .header__hamburger { 24 | @media (min-width: $breakpoint-l) { 25 | display: none; 26 | } 27 | } 28 | 29 | .header__title { 30 | display: inline-block; 31 | padding: 22px; 32 | color: white; 33 | margin: 0; 34 | } 35 | 36 | .header__hamburger { 37 | display: inline-block; 38 | float: left; 39 | cursor: pointer; 40 | z-index: 2; 41 | color: $primary-contrast; 42 | padding: 19px; 43 | opacity: .8; 44 | transition: opacity .15s; 45 | border-right: 1px solid rgba(black, .1); 46 | font-size: 22px; 47 | user-select: none; 48 | -webkit-font-smoothing: none; 49 | 50 | &:hover { 51 | opacity: 1; 52 | } 53 | 54 | @media (min-width: $breakpoint-xl) { 55 | display: none; 56 | } 57 | } 58 | 59 | /** 60 | * The navigation 61 | */ 62 | 63 | .header__nav { 64 | position: relative; 65 | transition: opacity .15s ease; 66 | z-index: 5; 67 | float: left; 68 | display: none; 69 | opacity: 1; 70 | border-bottom: 1px solid rgba(black, .1); 71 | 72 | @media (min-width: $breakpoint-l) { 73 | display: block; 74 | } 75 | } 76 | 77 | .header__nav__item { 78 | display: inline-block; 79 | border-right: 1px solid rgba(black, .1); 80 | } 81 | 82 | .header__nav__item__link { 83 | color: $primary-contrast; 84 | padding: 0 20px; 85 | line-height: 59px; 86 | text-decoration: none; 87 | font-weight: 600; 88 | display: block; 89 | transition: background-color .15s; 90 | 91 | &:hover { 92 | text-decoration: underline; 93 | } 94 | 95 | &.is-active { 96 | background-color: rgba(0, 0, 0, .1); 97 | } 98 | } 99 | 100 | /** 101 | * Project dropdown 102 | */ 103 | 104 | .header__meta { 105 | display: inline-block; 106 | color: $primary-contrast; 107 | border-left: 1px solid rgba(black, .1); 108 | padding-top: 5px; 109 | padding-right: 5px; 110 | float: right; 111 | position: absolute; 112 | right: 0; 113 | bottom: 0; 114 | top: 0; 115 | z-index: 1; 116 | position: relative; 117 | user-select: none; 118 | height: 100%; 119 | transition: background-color .15s; 120 | } 121 | 122 | .header__meta__info { 123 | float: left; 124 | padding: 11px 20px 0 20px; 125 | text-align: right; 126 | font-size: 11px; 127 | 128 | @media (min-width: $breakpoint-s) { 129 | font-size: 13px; 130 | } 131 | } 132 | 133 | .header__meta__info__title { 134 | font-weight: 600; 135 | } 136 | 137 | .header__meta__info__domain { 138 | color: $primary-contrast; 139 | margin-bottom: 5px; 140 | } 141 | 142 | .header__meta__info__version { 143 | display: inline-block; 144 | margin-left: 5px; 145 | } 146 | 147 | 148 | .header__meta__avatar { 149 | height: 50px; 150 | display: inline-block; 151 | } 152 | 153 | 154 | /** 155 | * Scrolled view 156 | */ 157 | 158 | .is-scrolled-in-section { 159 | 160 | .header__nav { 161 | opacity: 0; 162 | pointer-events: none; 163 | } 164 | 165 | @media (max-width: $breakpoint-s) { 166 | .header__meta { 167 | display: none; 168 | } 169 | } 170 | 171 | .scrolltop { 172 | transform: translate3d(0,0,0); 173 | } 174 | } -------------------------------------------------------------------------------- /template/styles/components/_scrolltop.scss: -------------------------------------------------------------------------------- 1 | .scrolltop { 2 | position: fixed; 3 | bottom: 10px; 4 | right: 10px; 5 | background: rgba(#fff, .95); 6 | border: 1px solid $grey; 7 | width: 45px; 8 | height: 45px; 9 | line-height: 50px; 10 | text-align: center; 11 | color: #ccc; 12 | font-weight: bold; 13 | z-index: 10; 14 | box-shadow: 0 2px 2px rgba(black, .1); 15 | transform: translate3d(0,200%,0); 16 | transition: transform .2s ease-out, border-color .15s; 17 | 18 | &::before { 19 | content: ''; 20 | display: block; 21 | width: 0; 22 | height: 0; 23 | border-style: solid; 24 | border-width: 7px 7px 8px 7px; 25 | border-color: transparent transparent $grey transparent; 26 | position: absolute; 27 | left: 50%; 28 | top: 10px; 29 | transform: translateX(-50%); 30 | transition: border-color .15s; 31 | } 32 | 33 | &::after { 34 | content: ''; 35 | display: block; 36 | width: 0; 37 | height: 0; 38 | border-style: solid; 39 | border-width: 0 4px 5px 4px; 40 | border-color: transparent transparent #fff transparent; 41 | position: absolute; 42 | left: 50%; 43 | top: 21px; 44 | transform: translateX(-50%); 45 | } 46 | 47 | &:hover { 48 | border-color: #aaa; 49 | 50 | &::before { 51 | border-bottom-color: #aaa; 52 | } 53 | } 54 | } -------------------------------------------------------------------------------- /template/styles/components/_sidebar.scss: -------------------------------------------------------------------------------- 1 | .sidebar { 2 | position: fixed; 3 | width: $sidebar-width; 4 | bottom: 0; 5 | top: 0; 6 | left: 0; 7 | background-color: $bg-grey; 8 | z-index: 3; 9 | transition: transform .15s; 10 | transform: translateX(-$sidebar-width); 11 | overflow-y: auto; 12 | padding-top: 60px; 13 | 14 | @media (min-width: $breakpoint-xl) { 15 | padding-top: 0; 16 | transform: none; 17 | border-right: 1px solid #ddd; 18 | } 19 | 20 | .is-open & { 21 | transform: scale(0, 100%); 22 | } 23 | } 24 | 25 | .has-no-headings .sidebar { 26 | @media (min-width: $breakpoint-xl) { 27 | display: none; 28 | } 29 | } 30 | 31 | 32 | /** 33 | * Sidebar list pages 34 | */ 35 | 36 | .sidebar__nav { 37 | @media (min-width: $breakpoint-l) { 38 | display: none; 39 | } 40 | } 41 | 42 | .sidebar__nav__item__link { 43 | display: block; 44 | text-decoration: none; 45 | margin-bottom: 0; 46 | color: $primary-contrast; 47 | font-weight: bold; 48 | background-color: $primary; 49 | border-bottom: 1px solid rgba(black, .15); 50 | padding: 22px; 51 | position: relative; 52 | 53 | &:hover { 54 | text-decoration: underline; 55 | } 56 | 57 | .is-active & { 58 | &::after { 59 | position: absolute; 60 | top: 0; 61 | right: 0; 62 | bottom: 0; 63 | left: 0; 64 | display: block; 65 | content: ''; 66 | background-color: rgba(0, 0, 0, .1); 67 | } 68 | } 69 | } 70 | 71 | /** 72 | * Sidebar list headings 73 | */ 74 | 75 | .sidebar__inpage { 76 | padding-top: 10px; 77 | } 78 | 79 | .sidebar__inpage__item__link { 80 | display: block; 81 | text-decoration: none; 82 | color: $text; 83 | font-size: 14px; 84 | line-height: 1.4; 85 | margin-bottom: 0; 86 | padding: 10px; 87 | width: 100%; 88 | letter-spacing: -.07px; 89 | white-space: nowrap; 90 | overflow: hidden; 91 | text-overflow: ellipsis; 92 | 93 | &:hover { 94 | text-decoration: underline; 95 | } 96 | 97 | .is-active & { 98 | font-weight: 600; 99 | } 100 | } 101 | 102 | 103 | /** 104 | * Toggles 105 | */ 106 | 107 | .hamburger-toggle { 108 | display: none; 109 | } 110 | 111 | .hamburger-toggle:checked ~ .sidebar { 112 | transform: translateX(0); 113 | } 114 | 115 | .close-projects, 116 | .close-sidebar { 117 | position: fixed; 118 | bottom: 0; 119 | top: 0; 120 | right: 0; 121 | left: 0; 122 | background-color: black; 123 | opacity: 0; 124 | visibility: hidden; 125 | z-index: 2; 126 | cursor: pointer; 127 | transition: opacity .5s; 128 | } 129 | 130 | .hamburger-toggle:checked ~ .close-sidebar { 131 | visibility: visible; 132 | opacity: .5; 133 | } -------------------------------------------------------------------------------- /template/styles/components/_table-of-contents.scss: -------------------------------------------------------------------------------- 1 | /** 2 | * Little list of links generated by js 3 | * Simply displays as a bunch of links next to each other. 4 | */ 5 | 6 | .content { 7 | .table-of-contents__list { 8 | border-bottom: 1px solid $grey; 9 | margin: 0 0 50px 0; 10 | padding: 0 0 50px 0; 11 | list-style: none; 12 | 13 | @media (min-width: $breakpoint-m) { 14 | column-count: 2; 15 | column-gap: 20px; 16 | } 17 | 18 | @media (min-width: $breakpoint-l) { 19 | column-count: 4; 20 | } 21 | } 22 | 23 | .table-of-contents__list__item { 24 | font-size: 14px; 25 | margin-bottom: 10px; 26 | line-height: 20px; 27 | vertical-align: top; 28 | width: 100%; 29 | display: inline-block; 30 | vertical-align: top; 31 | } 32 | 33 | .table-of-contents__list__item--error { 34 | text-decoration: line-through; 35 | } 36 | 37 | .table-of-contents__list__item__link { 38 | text-decoration: none; 39 | margin: 0; 40 | padding: 0; 41 | 42 | &:hover { 43 | text-decoration: underline; 44 | } 45 | 46 | svg { 47 | fill: $text; 48 | display: inline-block; 49 | width: 6px; 50 | height: 9px; 51 | margin-right: 6px; 52 | margin-top: -2px; 53 | vertical-align: middle; 54 | } 55 | } 56 | } 57 | 58 | #contents { 59 | display: none; 60 | } -------------------------------------------------------------------------------- /template/templates/_component.pug: -------------------------------------------------------------------------------- 1 | //- @pugdoc 2 | name: Component preview 3 | 4 | mixin component-preview 5 | .component__preview(class="js-component-preview") 6 | span.component__preview__handle(class="js-component-preview-handle") 7 | | 8 | 9 | iframe.component__preview__iframe(data-src=component.libFile frameborder="0" scrolling="no", height=component.height) 10 | 11 | 12 | //- @pugdoc 13 | name: Component meta 14 | 15 | mixin component-meta 16 | .component__meta 17 | .component__meta__text 18 | h3.component__meta__text__title #{component.meta.name} 19 | 20 | if component.meta.description 21 | .component__meta__text__description !{component.meta.description} 22 | 23 | .component__meta__buttons 24 | label.component__meta__buttons__code(for=('toggle-' + component.slug)) code 25 | 26 | a.component__meta__buttons__link(href=component.libFile, target="_blank") 27 | | 28 | 29 | 30 | //- @pugdoc 31 | name: Component code 32 | 33 | mixin component-code 34 | input(type='checkbox' name='source' id=('toggle-' + component.slug) class="component__toggle js-code-toggle", data-checked=(component.embedOptions.indexOf('code') > -1) ? 'true' : 'false') 35 | .component__code 36 | div.component__code__output 37 | pre 38 | code.language-markup 39 | 40 | 41 | //- components 42 | .component-wrapper 43 | if !component.meta 44 | .component(class="js-section has-error", id=(component.slug)) 45 | h3.component__title #{component.slug} 46 | .component__meta 47 | 48 | else 49 | - var cls = ['js-section', 'is-loading']; 50 | if component.embedOptions.indexOf('frameless') > -1 51 | - cls.push('is-frameless'); 52 | 53 | .component(class=cls, id=(component.slug)) 54 | +component-meta 55 | +component-code 56 | +component-preview 57 | 58 | if component.embedOptions.indexOf('frameless') > -1 59 | span.component__caption #{component.meta.name} 60 | -------------------------------------------------------------------------------- /template/templates/_table-of-contents.pug: -------------------------------------------------------------------------------- 1 | //- table of contents 2 | .table-of-contents 3 | ul.table-of-contents__list 4 | each component in locals.components 5 | li.table-of-contents__list__item(class=(component.hasError ? 'table-of-contents__list__item--error' : null)) 6 | a.table-of-contents__list__item__link(href=('#' + component.slug)) 7 | | 8 | | 9 | | #{component.name} -------------------------------------------------------------------------------- /test/autofill/autofill.md: -------------------------------------------------------------------------------- 1 | !{...autofill} 2 | -------------------------------------------------------------------------------- /test/autofill/components.md: -------------------------------------------------------------------------------- 1 | ${component1} 2 | -------------------------------------------------------------------------------- /test/autofill/test.js: -------------------------------------------------------------------------------- 1 | const test = require("ava"); 2 | const rimraf = require("rimraf"); 3 | const fs = require("fs"); 4 | 5 | const DM = require("../../lib/index"); 6 | 7 | const config = { 8 | output: __dirname + "/tmp/", 9 | pages: __dirname + "/", 10 | components: "./test/components.json", 11 | renderComponents: true, 12 | renderCSS: false, 13 | meta: { 14 | domain: "website.com", 15 | title: "Design Manual" 16 | } 17 | }; 18 | 19 | test.cb("autofill", t => { 20 | t.plan(3); 21 | rimraf.sync(__dirname + "/tmp/"); 22 | 23 | DM.build( 24 | Object.assign({}, config, { 25 | renderComponents: true, 26 | onLog: () => {}, 27 | onComplete: () => { 28 | setTimeout(() => { 29 | let componentsHtmlTmp = fs.readFileSync( 30 | config.output + "autofill.html", 31 | "utf8" 32 | ); 33 | t.truthy( 34 | componentsHtmlTmp.indexOf( 35 | '
' 36 | ) === -1 37 | ); 38 | t.truthy( 39 | componentsHtmlTmp.indexOf( 40 | '
' 41 | ) > -1 42 | ); 43 | t.truthy( 44 | componentsHtmlTmp.indexOf( 45 | '
' 46 | ) > -1 47 | ); 48 | t.end(); 49 | }, 1000); 50 | } 51 | }) 52 | ); 53 | }); 54 | -------------------------------------------------------------------------------- /test/component-html-options/page.md: -------------------------------------------------------------------------------- 1 | this is a page 2 | -------------------------------------------------------------------------------- /test/component-html-options/test.js: -------------------------------------------------------------------------------- 1 | const test = require("ava"); 2 | const fs = require("fs"); 3 | const rimraf = require("rimraf"); 4 | 5 | const DM = require("../../lib/index"); 6 | 7 | const config = { 8 | output: __dirname + "/tmp/", 9 | pages: __dirname + "/", 10 | components: "./test/components.json", 11 | meta: { 12 | domain: "website.com", 13 | title: "Design Manual" 14 | }, 15 | renderComponents: true, 16 | renderCSS: false 17 | }; 18 | 19 | test.cb("add head html", t => { 20 | t.plan(1); 21 | rimraf.sync(__dirname + "/tmp/head/"); 22 | 23 | DM.build( 24 | Object.assign({}, config, { 25 | output: config.output + "head/", 26 | componentHeadHtml: ` 27 | 28 | `, 29 | onLog: () => {}, 30 | onComplete: () => { 31 | setTimeout(() => { 32 | const componentsHtmlTmp = fs.readFileSync( 33 | config.output + "head/lib/component1.html", 34 | "utf8" 35 | ); 36 | t.truthy( 37 | componentsHtmlTmp.indexOf("") > -1 38 | ); 39 | t.end(); 40 | }, 1000); 41 | } 42 | }) 43 | ); 44 | }); 45 | 46 | test.cb("add body html", t => { 47 | t.plan(1); 48 | rimraf.sync(__dirname + "/tmp/body/"); 49 | 50 | DM.build( 51 | Object.assign({}, config, { 52 | output: config.output + "body/", 53 | componentBodyHtml: ` 54 | 55 | `, 56 | onLog: () => {}, 57 | onComplete: () => { 58 | setTimeout(() => { 59 | const componentsHtmlTmp = fs.readFileSync( 60 | config.output + "body/lib/component1.html", 61 | "utf8" 62 | ); 63 | t.truthy( 64 | componentsHtmlTmp.indexOf("") > -1 65 | ); 66 | t.end(); 67 | }, 1000); 68 | } 69 | }) 70 | ); 71 | }); 72 | -------------------------------------------------------------------------------- /test/components.json: -------------------------------------------------------------------------------- 1 | [ 2 | { 3 | "meta": { 4 | "name": "component1", 5 | "description": "

this is component 1

" 6 | }, 7 | "file": "path/to/component1.html", 8 | "output": "

this is component 1

" 9 | }, 10 | { 11 | "meta": { 12 | "name": "component2", 13 | "description": "

this is component 2

" 14 | }, 15 | "file": "path/to/component2.html", 16 | "output": "

this is component 2

" 17 | }, 18 | { 19 | "meta": { 20 | "name": "component3", 21 | "description": "

this is component 3

" 22 | }, 23 | "file": "path/to/component3.html", 24 | "output": "

this is component 3

" 25 | } 26 | ] 27 | -------------------------------------------------------------------------------- /test/components2.json: -------------------------------------------------------------------------------- 1 | [ 2 | { 3 | "meta": { 4 | "name": "component1", 5 | "description": "

this is component 1 changed

" 6 | }, 7 | "file": "path/to/component1.html", 8 | "output": "

this is component 1

" 9 | }, 10 | { 11 | "meta": { 12 | "name": "component2", 13 | "description": "

this is component 2

" 14 | }, 15 | "file": "path/to/component2.html", 16 | "output": "

this is component 2

" 17 | }, 18 | { 19 | "meta": { 20 | "name": "component3", 21 | "description": "

this is component 3

" 22 | }, 23 | "file": "path/to/component3.html", 24 | "output": "

this is component 3

" 25 | } 26 | ] 27 | -------------------------------------------------------------------------------- /test/config/page.md: -------------------------------------------------------------------------------- 1 | !{component1} 2 | -------------------------------------------------------------------------------- /test/config/page2.md: -------------------------------------------------------------------------------- 1 | foo 2 | -------------------------------------------------------------------------------- /test/config/test-body-html.js: -------------------------------------------------------------------------------- 1 | const test = require("ava"); 2 | const rimraf = require("rimraf"); 3 | const fs = require("fs"); 4 | const path = require("path"); 5 | const utils = require("../utils"); 6 | const buildAndMatchLogs = utils.buildAndMatchLogs; 7 | 8 | const config = { 9 | output: __dirname + "/tmp/", 10 | pages: __dirname + "/", 11 | components: "./test/components.json", 12 | meta: { 13 | domain: "website.com", 14 | title: "Design Manual" 15 | }, 16 | renderComponents: true, 17 | renderCSS: false 18 | }; 19 | 20 | /** 21 | * Test body html change 22 | */ 23 | 24 | test.cb("config: body html change", t => { 25 | t.plan(1); 26 | rimraf.sync(config.output); 27 | 28 | const expected = ` 29 | Starting design manual 30 | Detected changed configuration (body html) 31 | Forcing pages rebuild 32 | Starting components 33 | Found 0 changed components 34 | Generated components 35 | Starting pages 36 | Found 2 changed pages 37 | Generated test/config/tmp/page.html 38 | Generated test/config/tmp/page2.html 39 | Generated pages 40 | Design manual complete 41 | `; 42 | buildAndMatchLogs(null, config, null, () => { 43 | buildAndMatchLogs( 44 | t, 45 | Object.assign(config, { bodyHtml: "" }), 46 | expected 47 | ); 48 | }); 49 | }); 50 | -------------------------------------------------------------------------------- /test/config/test-component-body-html.js: -------------------------------------------------------------------------------- 1 | const test = require("ava"); 2 | const rimraf = require("rimraf"); 3 | const fs = require("fs"); 4 | const path = require("path"); 5 | const utils = require("../utils"); 6 | const buildAndMatchLogs = utils.buildAndMatchLogs; 7 | 8 | const config = { 9 | output: __dirname + "/tmp/", 10 | pages: __dirname + "/", 11 | components: "./test/components.json", 12 | meta: { 13 | domain: "website.com", 14 | title: "Design Manual" 15 | }, 16 | renderComponents: true, 17 | renderCSS: false 18 | }; 19 | 20 | /** 21 | * Test component body html change 22 | */ 23 | 24 | test.cb("config: component body html change", t => { 25 | t.plan(1); 26 | rimraf.sync(config.output); 27 | 28 | const expected = ` 29 | Starting design manual 30 | Detected changed configuration (components body html) 31 | Forcing components rebuild 32 | Starting components 33 | Found 3 changed components 34 | Rendering component ./lib/component1.html 35 | Rendering component ./lib/component2.html 36 | Rendering component ./lib/component3.html 37 | Generated components 38 | Starting pages 39 | Found 1 changed page 40 | Generated test/config/tmp/page.html 41 | Generated pages 42 | Design manual complete 43 | `; 44 | buildAndMatchLogs(null, config, null, () => { 45 | buildAndMatchLogs( 46 | t, 47 | Object.assign(config, { componentBodyHtml: "" }), 48 | expected 49 | ); 50 | }); 51 | }); 52 | -------------------------------------------------------------------------------- /test/config/test-component-head-html.js: -------------------------------------------------------------------------------- 1 | const test = require("ava"); 2 | const rimraf = require("rimraf"); 3 | const fs = require("fs"); 4 | const path = require("path"); 5 | const utils = require("../utils"); 6 | const buildAndMatchLogs = utils.buildAndMatchLogs; 7 | 8 | const config = { 9 | output: __dirname + "/tmp/", 10 | pages: __dirname + "/", 11 | components: "./test/components.json", 12 | meta: { 13 | domain: "website.com", 14 | title: "Design Manual" 15 | }, 16 | renderComponents: true, 17 | renderCSS: false 18 | }; 19 | 20 | /** 21 | * Test component head html change 22 | */ 23 | 24 | test.cb("config: component head html change", t => { 25 | t.plan(1); 26 | rimraf.sync(config.output); 27 | 28 | const expected = ` 29 | Starting design manual 30 | Detected changed configuration (components head html) 31 | Forcing components rebuild 32 | Starting components 33 | Found 3 changed components 34 | Rendering component ./lib/component1.html 35 | Rendering component ./lib/component2.html 36 | Rendering component ./lib/component3.html 37 | Generated components 38 | Starting pages 39 | Found 1 changed page 40 | Generated test/config/tmp/page.html 41 | Generated pages 42 | Design manual complete 43 | `; 44 | 45 | buildAndMatchLogs(null, config, null, () => { 46 | buildAndMatchLogs( 47 | t, 48 | Object.assign(config, { componentHeadHtml: "" }), 49 | expected 50 | ); 51 | }); 52 | }); 53 | -------------------------------------------------------------------------------- /test/config/test-component.js: -------------------------------------------------------------------------------- 1 | const test = require("ava"); 2 | const rimraf = require("rimraf"); 3 | const fs = require("fs"); 4 | const path = require("path"); 5 | const utils = require("../utils"); 6 | const buildAndMatchLogs = utils.buildAndMatchLogs; 7 | 8 | const config = { 9 | output: __dirname + "/tmp/", 10 | pages: __dirname + "/", 11 | components: "./test/components.json", 12 | meta: { 13 | domain: "website.com", 14 | title: "Design Manual" 15 | }, 16 | renderComponents: true, 17 | renderCSS: false 18 | }; 19 | 20 | /** 21 | * Test component changes 22 | */ 23 | 24 | test.cb("config: component change", t => { 25 | t.plan(1); 26 | 27 | rimraf.sync(__dirname + "/tmp/"); 28 | 29 | const expected = ` 30 | Starting design manual 31 | Starting components 32 | Found 1 changed component 33 | Rendering component ./lib/component1.html 34 | Generated components 35 | Starting pages 36 | Found 1 changed page 37 | Generated test/config/tmp/page.html 38 | Generated pages 39 | Design manual complete 40 | `; 41 | 42 | buildAndMatchLogs( 43 | null, 44 | Object.assign(config, { components: "./test/components.json" }), 45 | null, 46 | () => { 47 | buildAndMatchLogs( 48 | t, 49 | Object.assign(config, { components: "./test/components2.json" }), 50 | expected 51 | ); 52 | } 53 | ); 54 | }); 55 | -------------------------------------------------------------------------------- /test/config/test-contentsflag.js: -------------------------------------------------------------------------------- 1 | const test = require("ava"); 2 | const rimraf = require("rimraf"); 3 | const fs = require("fs"); 4 | const path = require("path"); 5 | const utils = require("../utils"); 6 | const buildAndMatchLogs = utils.buildAndMatchLogs; 7 | 8 | const config = { 9 | output: __dirname + "/tmp/", 10 | pages: __dirname + "/", 11 | components: "./test/components.json", 12 | meta: { 13 | domain: "website.com", 14 | title: "Design Manual" 15 | }, 16 | renderComponents: true, 17 | renderCSS: false 18 | }; 19 | 20 | /** 21 | * Test contents flag change 22 | */ 23 | 24 | test.cb("config: contents flag change", t => { 25 | t.plan(1); 26 | rimraf.sync(config.output); 27 | 28 | const expected = ` 29 | Starting design manual 30 | Detected changed configuration (contents flag) 31 | Forcing pages rebuild 32 | Starting components 33 | Found 0 changed components 34 | Generated components 35 | Starting pages 36 | Found 2 changed pages 37 | Generated test/config/tmp/page.html 38 | Generated test/config/tmp/page2.html 39 | Generated pages 40 | Design manual complete 41 | `; 42 | 43 | buildAndMatchLogs(null, config, null, () => { 44 | buildAndMatchLogs( 45 | t, 46 | Object.assign(config, { contentsFlag: "beep" }), 47 | expected 48 | ); 49 | }); 50 | }); 51 | -------------------------------------------------------------------------------- /test/config/test-error.js: -------------------------------------------------------------------------------- 1 | const test = require("ava"); 2 | const rimraf = require("rimraf"); 3 | const fs = require("fs"); 4 | const path = require("path"); 5 | const utils = require("../utils"); 6 | const buildAndMatchLogs = utils.buildAndMatchLogs; 7 | 8 | const config = { 9 | output: __dirname + "/tmp/", 10 | pages: __dirname + "/", 11 | components: "./test/components.json", 12 | meta: { 13 | domain: "website.com", 14 | title: "Design Manual" 15 | }, 16 | renderComponents: true, 17 | renderCSS: false 18 | }; 19 | 20 | /** 21 | * Test error in config file 22 | */ 23 | 24 | test.cb("config: error in config", t => { 25 | t.plan(1); 26 | rimraf.sync(config.output); 27 | 28 | const expected = ` 29 | Starting design manual 30 | Error parsing previous configuration 31 | Forcing components rebuild 32 | Forcing pages rebuild 33 | Starting components 34 | Found 3 changed components 35 | Rendering component ./lib/component1.html 36 | Rendering component ./lib/component2.html 37 | Rendering component ./lib/component3.html 38 | Generated components 39 | Starting pages 40 | Found 2 changed pages 41 | Generated test/config/tmp/page.html 42 | Generated test/config/tmp/page2.html 43 | Generated pages 44 | Design manual complete 45 | `; 46 | 47 | buildAndMatchLogs(null, config, null, () => { 48 | fs.writeFileSync( 49 | path.resolve(config.output, "design-manual-config.json"), 50 | "foo" 51 | ); 52 | buildAndMatchLogs(t, config, expected); 53 | }); 54 | }); 55 | -------------------------------------------------------------------------------- /test/config/test-head-html.js: -------------------------------------------------------------------------------- 1 | const test = require("ava"); 2 | const rimraf = require("rimraf"); 3 | const fs = require("fs"); 4 | const path = require("path"); 5 | const utils = require("../utils"); 6 | const buildAndMatchLogs = utils.buildAndMatchLogs; 7 | 8 | const config = { 9 | output: __dirname + "/tmp/", 10 | pages: __dirname + "/", 11 | components: "./test/components.json", 12 | meta: { 13 | domain: "website.com", 14 | title: "Design Manual" 15 | }, 16 | renderComponents: true, 17 | renderCSS: false 18 | }; 19 | 20 | /** 21 | * Test head html change 22 | */ 23 | 24 | test.cb("config: head html change", t => { 25 | t.plan(1); 26 | rimraf.sync(config.output); 27 | 28 | const expected = ` 29 | Starting design manual 30 | Detected changed configuration (head html) 31 | Forcing pages rebuild 32 | Starting components 33 | Found 0 changed components 34 | Generated components 35 | Starting pages 36 | Found 2 changed pages 37 | Generated test/config/tmp/page.html 38 | Generated test/config/tmp/page2.html 39 | Generated pages 40 | Design manual complete 41 | `; 42 | 43 | buildAndMatchLogs(null, config, null, () => { 44 | buildAndMatchLogs( 45 | t, 46 | Object.assign(config, { headHtml: "" }), 47 | expected 48 | ); 49 | }); 50 | }); 51 | -------------------------------------------------------------------------------- /test/config/test-meta.js: -------------------------------------------------------------------------------- 1 | const test = require("ava"); 2 | const rimraf = require("rimraf"); 3 | const fs = require("fs"); 4 | const path = require("path"); 5 | const utils = require("../utils"); 6 | const buildAndMatchLogs = utils.buildAndMatchLogs; 7 | 8 | const config = { 9 | output: __dirname + "/tmp/", 10 | pages: __dirname + "/", 11 | components: "./test/components.json", 12 | meta: { 13 | domain: "website.com", 14 | title: "Design Manual" 15 | }, 16 | renderComponents: true, 17 | renderCSS: false 18 | }; 19 | 20 | /** 21 | * Test meta change 22 | */ 23 | 24 | const expectedMetaChange = ` 25 | Starting design manual 26 | Detected changed configuration (meta) 27 | Forcing pages rebuild 28 | Starting components 29 | Found 0 changed components 30 | Generated components 31 | Starting pages 32 | Found 2 changed pages 33 | Generated test/config/tmp/page.html 34 | Generated test/config/tmp/page2.html 35 | Generated pages 36 | Design manual complete 37 | `; 38 | 39 | test.cb("config: meta change", t => { 40 | t.plan(1); 41 | buildAndMatchLogs(null, config, null, () => { 42 | buildAndMatchLogs( 43 | t, 44 | Object.assign(config, { 45 | meta: { domain: "0", title: "0", avatar: "0", version: "0" } 46 | }), 47 | expectedMetaChange 48 | ); 49 | }); 50 | }); 51 | 52 | test.cb("config: meta change (domain)", t => { 53 | t.plan(1); 54 | buildAndMatchLogs( 55 | t, 56 | Object.assign(config, { 57 | meta: { domain: "1", title: "0", avatar: "0", version: "0" } 58 | }), 59 | expectedMetaChange 60 | ); 61 | }); 62 | 63 | test.cb("config: meta change (title)", t => { 64 | t.plan(1); 65 | buildAndMatchLogs( 66 | t, 67 | Object.assign(config, { 68 | meta: { domain: "1", title: "1", avatar: "0", version: "0" } 69 | }), 70 | expectedMetaChange 71 | ); 72 | }); 73 | 74 | test.cb("config: meta change (avatar)", t => { 75 | t.plan(1); 76 | buildAndMatchLogs( 77 | t, 78 | Object.assign(config, { 79 | meta: { domain: "1", title: "1", avatar: "1", version: "0" } 80 | }), 81 | expectedMetaChange 82 | ); 83 | }); 84 | 85 | test.cb("config: meta change (version)", t => { 86 | t.plan(1); 87 | buildAndMatchLogs( 88 | t, 89 | Object.assign(config, { 90 | meta: { domain: "1", title: "1", avatar: "1", version: "1" } 91 | }), 92 | expectedMetaChange 93 | ); 94 | }); 95 | -------------------------------------------------------------------------------- /test/config/test-nav.js: -------------------------------------------------------------------------------- 1 | const test = require("ava"); 2 | const rimraf = require("rimraf"); 3 | const fs = require("fs"); 4 | const path = require("path"); 5 | const utils = require("../utils"); 6 | const buildAndMatchLogs = utils.buildAndMatchLogs; 7 | 8 | const config = { 9 | output: __dirname + "/tmp/", 10 | pages: __dirname + "/", 11 | components: "./test/components.json", 12 | meta: { 13 | domain: "website.com", 14 | title: "Design Manual" 15 | }, 16 | renderComponents: true, 17 | renderCSS: false 18 | }; 19 | 20 | /** 21 | * Test nav change 22 | */ 23 | 24 | test.cb("config: nav change", t => { 25 | t.plan(1); 26 | rimraf.sync(config.output); 27 | 28 | const expected = ` 29 | Starting design manual 30 | Detected changed configuration (nav) 31 | Forcing pages rebuild 32 | Starting components 33 | Found 0 changed components 34 | Generated components 35 | Starting pages 36 | Found 2 changed pages 37 | Generated test/config/tmp/page.html 38 | Generated test/config/tmp/page2.html 39 | Generated pages 40 | Design manual complete 41 | `; 42 | 43 | buildAndMatchLogs(null, config, null, () => { 44 | buildAndMatchLogs( 45 | t, 46 | Object.assign(config, { nav: [{ label: "🏡", href: "/" }] }), 47 | expected 48 | ); 49 | }); 50 | }); 51 | -------------------------------------------------------------------------------- /test/config/test-new.js: -------------------------------------------------------------------------------- 1 | const test = require("ava"); 2 | const rimraf = require("rimraf"); 3 | const utils = require("../utils"); 4 | const buildAndMatchLogs = utils.buildAndMatchLogs; 5 | 6 | const config = { 7 | output: __dirname + "/tmp/", 8 | pages: __dirname + "/", 9 | components: "./test/components.json", 10 | meta: { 11 | domain: "website.com", 12 | title: "Design Manual" 13 | }, 14 | renderComponents: true, 15 | renderCSS: false 16 | }; 17 | 18 | /** 19 | * test if no configuration was found 20 | */ 21 | 22 | test.cb("config: new config", t => { 23 | t.plan(1); 24 | rimraf.sync(config.output); 25 | 26 | const expected = ` 27 | Starting design manual 28 | Could not find previous configuration 29 | Forcing components rebuild 30 | Forcing pages rebuild 31 | Starting components 32 | Found 3 changed components 33 | Rendering component ./lib/component1.html 34 | Rendering component ./lib/component2.html 35 | Rendering component ./lib/component3.html 36 | Generated components 37 | Starting pages 38 | Found 2 changed pages 39 | Generated test/config/tmp/page.html 40 | Generated test/config/tmp/page2.html 41 | Generated pages 42 | Design manual complete 43 | `; 44 | 45 | buildAndMatchLogs(t, config, expected); 46 | }); 47 | -------------------------------------------------------------------------------- /test/config/test-output.js: -------------------------------------------------------------------------------- 1 | const test = require("ava"); 2 | const rimraf = require("rimraf"); 3 | const fs = require("fs"); 4 | const path = require("path"); 5 | const utils = require("../utils"); 6 | const buildAndMatchLogs = utils.buildAndMatchLogs; 7 | 8 | const config = { 9 | output: __dirname + "/tmp/", 10 | pages: __dirname + "/", 11 | components: "./test/components.json", 12 | meta: { 13 | domain: "website.com", 14 | title: "Design Manual" 15 | }, 16 | renderComponents: true, 17 | renderCSS: false 18 | }; 19 | 20 | /** 21 | * Test output change, rebuild all 22 | */ 23 | 24 | test.cb("config: output change", t => { 25 | t.plan(1); 26 | rimraf.sync(__dirname + "/tmp2/"); 27 | 28 | const expected = ` 29 | Starting design manual 30 | Could not find previous configuration 31 | Forcing components rebuild 32 | Forcing pages rebuild 33 | Starting components 34 | Found 3 changed components 35 | Rendering component ./lib/component1.html 36 | Rendering component ./lib/component2.html 37 | Rendering component ./lib/component3.html 38 | Generated components 39 | Starting pages 40 | Found 2 changed pages 41 | Generated test/config/tmp2/page.html 42 | Generated test/config/tmp2/page2.html 43 | Generated pages 44 | Design manual complete 45 | `; 46 | 47 | buildAndMatchLogs(null, config, null, () => { 48 | buildAndMatchLogs( 49 | t, 50 | Object.assign(config, { output: __dirname + "/tmp2/" }), 51 | expected 52 | ); 53 | }); 54 | }); 55 | 56 | test.after.cb(t => { 57 | rimraf.sync(__dirname + "/tmp2/"); 58 | t.end(); 59 | }); 60 | -------------------------------------------------------------------------------- /test/config/test-prerender.js: -------------------------------------------------------------------------------- 1 | const test = require("ava"); 2 | const rimraf = require("rimraf"); 3 | const fs = require("fs"); 4 | const path = require("path"); 5 | const utils = require("../utils"); 6 | const buildAndMatchLogs = utils.buildAndMatchLogs; 7 | 8 | const config = { 9 | output: __dirname + "/tmp/", 10 | pages: __dirname + "/", 11 | components: "./test/components.json", 12 | meta: { 13 | domain: "website.com", 14 | title: "Design Manual" 15 | }, 16 | renderComponents: true, 17 | renderCSS: false 18 | }; 19 | 20 | /** 21 | * Test prerender change 22 | */ 23 | 24 | test.cb("config: prerender change", t => { 25 | t.plan(1); 26 | rimraf.sync(config.output); 27 | 28 | const expected = ` 29 | Starting design manual 30 | Detected changed configuration (prerender) 31 | Forcing pages rebuild 32 | Starting components 33 | Found 0 changed components 34 | Generated components 35 | Starting pages 36 | Found 2 changed pages 37 | Generated test/config/tmp/page.html 38 | Generated test/config/tmp/page2.html 39 | Generated pages 40 | Design manual complete 41 | `; 42 | 43 | buildAndMatchLogs(null, config, null, () => { 44 | buildAndMatchLogs(t, Object.assign(config, { prerender: 1 }), expected); 45 | }); 46 | }); 47 | -------------------------------------------------------------------------------- /test/config/test-same.js: -------------------------------------------------------------------------------- 1 | const test = require("ava"); 2 | const rimraf = require("rimraf"); 3 | const utils = require("../utils"); 4 | const buildAndMatchLogs = utils.buildAndMatchLogs; 5 | 6 | const config = { 7 | output: __dirname + "/tmp/", 8 | pages: __dirname + "/", 9 | components: "./test/components.json", 10 | meta: { 11 | domain: "website.com", 12 | title: "Design Manual" 13 | }, 14 | renderComponents: true, 15 | renderCSS: false 16 | }; 17 | 18 | /** 19 | * test if no configuration was found 20 | */ 21 | 22 | test.cb("config: new config", t => { 23 | t.plan(1); 24 | rimraf.sync(config.output); 25 | 26 | const expected = ` 27 | Starting design manual 28 | Starting components 29 | Found 0 changed components 30 | Generated components 31 | Starting pages 32 | Found 0 changed pages 33 | Generated pages 34 | Design manual complete 35 | `; 36 | 37 | buildAndMatchLogs(null, config, null, () => { 38 | buildAndMatchLogs(t, config, expected); 39 | }); 40 | }); 41 | -------------------------------------------------------------------------------- /test/config/test-validate.js: -------------------------------------------------------------------------------- 1 | const test = require("ava"); 2 | const rimraf = require("rimraf"); 3 | const fs = require("fs"); 4 | const path = require("path"); 5 | const utils = require("../utils"); 6 | const buildAndMatchLogs = utils.buildAndMatchLogs; 7 | 8 | const config = { 9 | output: __dirname + "/tmp/", 10 | pages: __dirname + "/", 11 | components: "./test/components.json", 12 | meta: { 13 | domain: "website.com", 14 | title: "Design Manual" 15 | }, 16 | renderComponents: true, 17 | renderCSS: false 18 | }; 19 | 20 | /** 21 | * Test mandatory options 22 | */ 23 | 24 | test.cb("config: validate output", t => { 25 | t.plan(1); 26 | 27 | const options = Object.assign({}, config, { output: null }); 28 | 29 | const expected = ` 30 | Design Manual error: options.output is required 31 | Design manual complete 32 | `; 33 | 34 | buildAndMatchLogs(t, options, expected); 35 | }); 36 | 37 | test.cb("config: validate pages", t => { 38 | t.plan(1); 39 | 40 | const options = Object.assign({}, config, { pages: null }); 41 | 42 | const expected = ` 43 | Design Manual error: options.pages is required 44 | Design manual complete 45 | `; 46 | 47 | buildAndMatchLogs(t, options, expected); 48 | }); 49 | 50 | test.cb("config: validate components", t => { 51 | t.plan(1); 52 | 53 | const options = Object.assign({}, config, { components: null }); 54 | 55 | const expected = ` 56 | Design Manual error: options.components is required 57 | Design manual complete 58 | `; 59 | 60 | buildAndMatchLogs(t, options, expected); 61 | }); 62 | 63 | test.cb("config: validate meta", t => { 64 | t.plan(1); 65 | 66 | const options = Object.assign({}, config, { meta: null }); 67 | 68 | const expected = ` 69 | Design Manual error: options.meta is required 70 | Design manual complete 71 | `; 72 | 73 | buildAndMatchLogs(t, options, expected); 74 | }); 75 | 76 | test.cb("config: validate meta.title", t => { 77 | t.plan(1); 78 | 79 | const options = Object.assign({}, config, { 80 | meta: { title: null, domain: "foo" } 81 | }); 82 | 83 | const expected = ` 84 | Design Manual error: options.meta.title is required 85 | Design manual complete 86 | `; 87 | 88 | buildAndMatchLogs(t, options, expected); 89 | }); 90 | 91 | test.cb("config: validate meta.domain", t => { 92 | t.plan(1); 93 | 94 | const options = Object.assign({}, config, { 95 | meta: { title: "foo", domain: null } 96 | }); 97 | 98 | const expected = ` 99 | Design Manual error: options.meta.domain is required 100 | Design manual complete 101 | `; 102 | 103 | buildAndMatchLogs(t, options, expected); 104 | }); 105 | -------------------------------------------------------------------------------- /test/defaults/components.md: -------------------------------------------------------------------------------- 1 | # Components 2 | 3 | This is the components page. 4 | 5 | --- 6 | 7 | ## Components 8 | 9 | These are components 10 | 11 | ### Contents 12 | 13 | !{component1} 14 | !{component2} 15 | !{component3} 16 | -------------------------------------------------------------------------------- /test/defaults/test.js: -------------------------------------------------------------------------------- 1 | const test = require("ava"); 2 | const rimraf = require("rimraf"); 3 | 4 | const isThere = require("is-there"); 5 | const DM = require("../../lib/index"); 6 | 7 | const config = { 8 | output: __dirname + "/tmp/", 9 | pages: __dirname + "/", 10 | components: "./test/components.json", 11 | meta: { 12 | domain: "website.com", 13 | title: "Design Manual" 14 | } 15 | }; 16 | 17 | test.cb("defaults", t => { 18 | t.plan(7); 19 | rimraf.sync(__dirname + "/tmp"); 20 | 21 | DM.build( 22 | Object.assign({}, config, { 23 | onLog: () => {}, 24 | onComplete: () => { 25 | setTimeout(() => { 26 | // test styles/js generated 27 | t.true(isThere(config.output + "all.min.css"), "css exists"); 28 | 29 | // test register files generated 30 | t.true( 31 | isThere(config.output + "design-manual-components.json"), 32 | "design manual components json exists" 33 | ); 34 | t.true( 35 | isThere(config.output + "design-manual-config.json"), 36 | "design manual config json exists" 37 | ); 38 | 39 | // test page generated 40 | t.true( 41 | isThere(config.output + "components.html"), 42 | "components.html exists" 43 | ); 44 | 45 | // test lib files generated 46 | t.true( 47 | isThere(config.output + "lib/component1.html"), 48 | "component1.html exists" 49 | ); 50 | t.true( 51 | isThere(config.output + "lib/component2.html"), 52 | "component2.html exists" 53 | ); 54 | t.true( 55 | isThere(config.output + "lib/component3.html"), 56 | "component3.html exists" 57 | ); 58 | 59 | t.end(); 60 | }, 1000); 61 | } 62 | }) 63 | ); 64 | }); 65 | -------------------------------------------------------------------------------- /test/gulp/test.js: -------------------------------------------------------------------------------- 1 | const test = require("ava"); 2 | const rimraf = require("rimraf"); 3 | const gulp = require("gulp"); 4 | 5 | const DM = require("../../lib/index"); 6 | 7 | const config = { 8 | output: __dirname + "/tmp/", 9 | pages: __dirname + "/", 10 | components: "./test/components.json", 11 | meta: { 12 | domain: "website.com", 13 | title: "Design Manual" 14 | } 15 | }; 16 | 17 | test.cb("gulp", t => { 18 | t.plan(1); 19 | rimraf.sync(__dirname + "/tmp"); 20 | 21 | setTimeout(() => { 22 | gulp.task("design-manual")(() => { 23 | console.log("first: should never finish"); 24 | t.fail(); 25 | t.end(); 26 | }); 27 | }, 0); 28 | 29 | setTimeout(() => { 30 | gulp.task("design-manual")(() => { 31 | console.log("third: should finish"); 32 | t.pass(); 33 | t.end(); 34 | }); 35 | }, 250); 36 | 37 | setTimeout(() => { 38 | gulp.task("design-manual")(() => { 39 | console.log("second: should never finish"); 40 | t.fail(); 41 | t.end(); 42 | }); 43 | }, 200); 44 | }); 45 | 46 | gulp.task("design-manual", done => { 47 | DM.build( 48 | Object.assign({}, config, { 49 | onLog: () => {}, 50 | onComplete: function() { 51 | done(); 52 | } 53 | }) 54 | ); 55 | }); 56 | -------------------------------------------------------------------------------- /test/interrupt/components.md: -------------------------------------------------------------------------------- 1 | # Components 2 | 3 | This is the components page. 4 | 5 | --- 6 | 7 | ## Components 8 | 9 | These are components 10 | 11 | ### Contents 12 | 13 | !{component1} 14 | !{component2} 15 | !{component3} 16 | -------------------------------------------------------------------------------- /test/interrupt/test.js: -------------------------------------------------------------------------------- 1 | const test = require("ava"); 2 | const rimraf = require("rimraf"); 3 | 4 | const DM = require("../../lib/index"); 5 | 6 | const config = { 7 | output: __dirname + "/tmp/", 8 | pages: __dirname + "/", 9 | components: "./test/components.json", 10 | meta: { 11 | domain: "website.com", 12 | title: "Design Manual" 13 | } 14 | }; 15 | 16 | test.cb("interrupt", t => { 17 | t.plan(1); 18 | rimraf.sync(__dirname + "/tmp"); 19 | 20 | DM.build( 21 | Object.assign({}, config, { 22 | onLog: () => {}, 23 | onComplete: () => { 24 | t.fail(); 25 | t.end(); 26 | } 27 | }) 28 | ); 29 | 30 | setTimeout(() => { 31 | DM.build( 32 | Object.assign({}, config, { 33 | onLog: () => {}, 34 | onComplete: () => { 35 | t.pass(); 36 | t.end(); 37 | } 38 | }) 39 | ); 40 | }, 250); 41 | 42 | setTimeout(() => { 43 | DM.build( 44 | Object.assign({}, config, { 45 | onLog: () => {}, 46 | onComplete: () => { 47 | t.fail(); 48 | t.end(); 49 | } 50 | }) 51 | ); 52 | }, 200); 53 | }); 54 | -------------------------------------------------------------------------------- /test/page-html-options/page.md: -------------------------------------------------------------------------------- 1 | this is a page 2 | -------------------------------------------------------------------------------- /test/page-html-options/test.js: -------------------------------------------------------------------------------- 1 | const test = require("ava"); 2 | const fs = require("fs"); 3 | const rimraf = require("rimraf"); 4 | 5 | const DM = require("../../lib/index"); 6 | 7 | const config = { 8 | output: __dirname + "/tmp/", 9 | pages: __dirname + "/", 10 | components: "./test/components.json", 11 | meta: { 12 | domain: "website.com", 13 | title: "Design Manual" 14 | }, 15 | renderComponents: false, 16 | renderCSS: false 17 | }; 18 | 19 | test.cb("add head html", t => { 20 | t.plan(1); 21 | rimraf.sync(__dirname + "/tmp/head/"); 22 | 23 | DM.build( 24 | Object.assign({}, config, { 25 | output: config.output + "head/", 26 | headHtml: "", 27 | onLog: () => {}, 28 | onComplete: () => { 29 | setTimeout(() => { 30 | let componentsHtmlTmp = fs.readFileSync( 31 | config.output + "head/page.html", 32 | "utf8" 33 | ); 34 | t.truthy(componentsHtmlTmp.indexOf("") > -1); 35 | 36 | t.end(); 37 | }, 1000); 38 | } 39 | }) 40 | ); 41 | }); 42 | 43 | test.cb("add body html", t => { 44 | t.plan(1); 45 | rimraf.sync(__dirname + "/tmp/body/"); 46 | 47 | DM.build( 48 | Object.assign({}, config, { 49 | output: config.output + "body/", 50 | bodyHtml: '', 51 | onLog: () => {}, 52 | onComplete: () => { 53 | setTimeout(() => { 54 | let componentsHtmlTmp = fs.readFileSync( 55 | config.output + "body/page.html", 56 | "utf8" 57 | ); 58 | t.truthy( 59 | componentsHtmlTmp.indexOf('') > -1 60 | ); 61 | t.end(); 62 | }, 1000); 63 | } 64 | }) 65 | ); 66 | }); 67 | -------------------------------------------------------------------------------- /test/prerender/page.md: -------------------------------------------------------------------------------- 1 | # Components 2 | 3 | This is the components page. 4 | 5 | --- 6 | 7 | ## Components 8 | 9 | These are components 10 | 11 | ### Contents 12 | 13 | !{component1} 14 | !{component3} 15 | -------------------------------------------------------------------------------- /test/prerender/test.js: -------------------------------------------------------------------------------- 1 | const test = require("ava"); 2 | const rimraf = require("rimraf"); 3 | const fs = require("fs"); 4 | 5 | const DM = require("../../lib/index"); 6 | 7 | const config = { 8 | output: __dirname + "/tmp/", 9 | pages: __dirname + "/", 10 | components: "./test/components.json", 11 | renderComponents: true, 12 | renderCSS: false, 13 | prerender: { 14 | port: 3000, 15 | path: "", 16 | serveFolder: __dirname + "/tmp/" 17 | }, 18 | meta: { 19 | domain: "website.com", 20 | title: "Design Manual" 21 | } 22 | }; 23 | 24 | test.cb("render pages with components and get their heights set", t => { 25 | t.plan(2); 26 | rimraf.sync(__dirname + "/tmp"); 27 | 28 | DM.build( 29 | Object.assign({}, config, { 30 | onLog: () => {}, 31 | onComplete: () => { 32 | setTimeout(() => { 33 | let componentsHtmlTmp = fs.readFileSync( 34 | config.output + "page.html", 35 | "utf8" 36 | ); 37 | t.truthy( 38 | componentsHtmlTmp.indexOf('scrolling="no" height="70"') > -1 39 | ); 40 | t.truthy( 41 | componentsHtmlTmp.indexOf('scrolling="no" height="163"') > -1 42 | ); 43 | 44 | t.end(); 45 | }, 1000); 46 | } 47 | }) 48 | ); 49 | }); 50 | 51 | test.cb("render pages with components and don't prerender", t => { 52 | t.plan(2); 53 | rimraf.sync(__dirname + "/tmp"); 54 | 55 | DM.build( 56 | Object.assign({}, config, { 57 | prerender: false, 58 | onLog: () => {}, 59 | onComplete: () => { 60 | setTimeout(() => { 61 | let componentsHtmlTmp = fs.readFileSync( 62 | config.output + "page.html", 63 | "utf8" 64 | ); 65 | t.truthy( 66 | componentsHtmlTmp.indexOf( 67 | '' 68 | ) > -1 69 | ); 70 | t.truthy( 71 | componentsHtmlTmp.indexOf( 72 | '' 73 | ) > -1 74 | ); 75 | 76 | t.end(); 77 | }, 1000); 78 | } 79 | }) 80 | ); 81 | }); 82 | -------------------------------------------------------------------------------- /test/puppeteer/222.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | Document 8 | 9 | 10 | 11 | 14 | 15 | 16 | -------------------------------------------------------------------------------- /test/puppeteer/test.js: -------------------------------------------------------------------------------- 1 | const test = require("ava"); 2 | const serveStatic = require("serve-static"); 3 | const cp = require("child_process"); 4 | const http = require("http"); 5 | 6 | test("get components height", async t => { 7 | const height = new Promise((resolve, reject) => { 8 | // static file server 9 | const serve = serveStatic(__dirname); 10 | const server = http.createServer((req, res) => { 11 | serve(req, res, function(err) { 12 | res.statusCode = err ? err.status || 500 : 404; 13 | res.end(err ? err.stack : "sorry!"); 14 | }); 15 | }); 16 | 17 | server.on("listening", () => { 18 | let puppet = cp.fork(`lib/puppeteer.js`); 19 | 20 | puppet.once("message", function(data) { 21 | if (data === "puppeteer-ready") { 22 | puppet.once("message", function(data) { 23 | if (data.height) { 24 | resolve(data.height); 25 | } 26 | }); 27 | 28 | puppet.send({ 29 | url: "http://localhost:3000/222.html" 30 | }); 31 | } 32 | }); 33 | }); 34 | 35 | server.listen(3000); 36 | }); 37 | 38 | t.is(await height, 222); 39 | }); 40 | -------------------------------------------------------------------------------- /test/render-css/test.js: -------------------------------------------------------------------------------- 1 | const test = require("ava"); 2 | const rimraf = require("rimraf"); 3 | 4 | const isThere = require("is-there"); 5 | const DM = require("../../lib/index"); 6 | 7 | const config = { 8 | output: __dirname + "/tmp/", 9 | pages: __dirname + "/", 10 | components: "./test/components.json", 11 | renderComponents: false, 12 | renderCSS: false, 13 | meta: { 14 | domain: "website.com", 15 | title: "Design Manual" 16 | } 17 | }; 18 | 19 | test.cb("render css", t => { 20 | t.plan(1); 21 | rimraf.sync(__dirname + "/tmp"); 22 | 23 | DM.build( 24 | Object.assign({}, config, { 25 | renderCSS: true, 26 | onLog: () => {}, 27 | onComplete: () => { 28 | t.true(isThere(config.output + "all.min.css"), "css exists"); 29 | t.end(); 30 | } 31 | }) 32 | ); 33 | }); 34 | 35 | test.cb("don't render css", t => { 36 | t.plan(1); 37 | rimraf.sync(__dirname + "/tmp"); 38 | 39 | DM.build( 40 | Object.assign({}, config, { 41 | renderCSS: false, 42 | onLog: () => {}, 43 | onComplete: () => { 44 | t.true(!isThere(config.output + "app.min.js"), "css does not exist"); 45 | t.end(); 46 | } 47 | }) 48 | ); 49 | }); 50 | -------------------------------------------------------------------------------- /test/render-pages/code-frameless.md: -------------------------------------------------------------------------------- 1 | $${component1} 2 | -------------------------------------------------------------------------------- /test/render-pages/code.md: -------------------------------------------------------------------------------- 1 | ${component1} 2 | -------------------------------------------------------------------------------- /test/render-pages/component-frameless.md: -------------------------------------------------------------------------------- 1 | !!{component1} 2 | -------------------------------------------------------------------------------- /test/render-pages/component.md: -------------------------------------------------------------------------------- 1 | !{component1} 2 | -------------------------------------------------------------------------------- /test/render-pages/page.md: -------------------------------------------------------------------------------- 1 | # Components 2 | 3 | This is the components page. 4 | 5 | --- 6 | 7 | ## Components 8 | 9 | These are components 10 | 11 | ### Contents 12 | 13 | !{component1} 14 | !{component2} 15 | !!{component3} 16 | ${component3} 17 | $${component3} 18 | -------------------------------------------------------------------------------- /test/render-pages/test.js: -------------------------------------------------------------------------------- 1 | const test = require("ava"); 2 | const rimraf = require("rimraf"); 3 | const fs = require("fs"); 4 | 5 | const DM = require("../../lib/index"); 6 | 7 | const config = { 8 | output: __dirname + "/tmp/", 9 | pages: __dirname + "/", 10 | components: "./test/components.json", 11 | renderComponents: true, 12 | renderCSS: false, 13 | meta: { 14 | domain: "website.com", 15 | title: "Design Manual" 16 | } 17 | }; 18 | 19 | test.cb("render pages without components", t => { 20 | t.plan(3); 21 | rimraf.sync(__dirname + "/tmp/"); 22 | 23 | DM.build( 24 | Object.assign({}, config, { 25 | renderComponents: false, 26 | onLog: () => {}, 27 | onComplete: () => { 28 | setTimeout(() => { 29 | let componentsHtmlTmp = fs.readFileSync( 30 | config.output + "page.html", 31 | "utf8" 32 | ); 33 | t.truthy( 34 | componentsHtmlTmp.indexOf( 35 | '

component1

' 36 | ) > -1 37 | ); 38 | t.truthy( 39 | componentsHtmlTmp.indexOf( 40 | '

component2

' 41 | ) > -1 42 | ); 43 | t.truthy( 44 | componentsHtmlTmp.indexOf( 45 | '

component3

' 46 | ) > -1 47 | ); 48 | 49 | t.end(); 50 | }, 1000); 51 | } 52 | }) 53 | ); 54 | }); 55 | 56 | test.cb("render component", t => { 57 | t.plan(2); 58 | rimraf.sync(__dirname + "/tmp/"); 59 | 60 | DM.build( 61 | Object.assign({}, config, { 62 | renderComponents: true, 63 | onLog: () => {}, 64 | onComplete: () => { 65 | setTimeout(() => { 66 | let componentsHtmlTmp = fs.readFileSync( 67 | config.output + "component.html", 68 | "utf8" 69 | ); 70 | t.truthy( 71 | componentsHtmlTmp.indexOf( 72 | '
' 73 | ) > -1 74 | ); 75 | t.truthy( 76 | componentsHtmlTmp.indexOf( 77 | '' 78 | ) > -1 79 | ); 80 | 81 | t.end(); 82 | }, 1000); 83 | } 84 | }) 85 | ); 86 | }); 87 | 88 | test.cb("render component frameless", t => { 89 | t.plan(2); 90 | rimraf.sync(__dirname + "/tmp/"); 91 | 92 | DM.build( 93 | Object.assign({}, config, { 94 | renderComponents: true, 95 | onLog: () => {}, 96 | onComplete: () => { 97 | setTimeout(() => { 98 | let componentsHtmlTmp = fs.readFileSync( 99 | config.output + "component-frameless.html", 100 | "utf8" 101 | ); 102 | t.truthy( 103 | componentsHtmlTmp.indexOf( 104 | '
' 105 | ) > -1 106 | ); 107 | t.truthy( 108 | componentsHtmlTmp.indexOf( 109 | '' 110 | ) > -1 111 | ); 112 | 113 | t.end(); 114 | }, 1000); 115 | } 116 | }) 117 | ); 118 | }); 119 | 120 | test.cb("render code", t => { 121 | t.plan(3); 122 | rimraf.sync(__dirname + "/tmp/"); 123 | 124 | DM.build( 125 | Object.assign({}, config, { 126 | renderComponents: true, 127 | onLog: () => {}, 128 | onComplete: () => { 129 | setTimeout(() => { 130 | let componentsHtmlTmp = fs.readFileSync( 131 | config.output + "code.html", 132 | "utf8" 133 | ); 134 | t.truthy( 135 | componentsHtmlTmp.indexOf( 136 | '
' 137 | ) > -1 138 | ); 139 | t.truthy( 140 | componentsHtmlTmp.indexOf( 141 | '' 142 | ) > -1 143 | ); 144 | t.truthy( 145 | componentsHtmlTmp.indexOf( 146 | ' -1 148 | ); 149 | 150 | t.end(); 151 | }, 1000); 152 | } 153 | }) 154 | ); 155 | }); 156 | 157 | test.cb("render code frameless", t => { 158 | t.plan(3); 159 | rimraf.sync(__dirname + "/tmp/"); 160 | 161 | DM.build( 162 | Object.assign({}, config, { 163 | renderComponents: true, 164 | onLog: () => {}, 165 | onComplete: () => { 166 | setTimeout(() => { 167 | let componentsHtmlTmp = fs.readFileSync( 168 | config.output + "code-frameless.html", 169 | "utf8" 170 | ); 171 | t.truthy( 172 | componentsHtmlTmp.indexOf( 173 | '
' 174 | ) > -1 175 | ); 176 | t.truthy( 177 | componentsHtmlTmp.indexOf( 178 | '' 179 | ) > -1 180 | ); 181 | t.truthy( 182 | componentsHtmlTmp.indexOf( 183 | ' -1 185 | ); 186 | 187 | t.end(); 188 | }, 1000); 189 | } 190 | }) 191 | ); 192 | }); 193 | 194 | test.cb("render table of contents", t => { 195 | t.plan(4); 196 | rimraf.sync(__dirname + "/tmp/"); 197 | 198 | DM.build( 199 | Object.assign({}, config, { 200 | renderComponents: true, 201 | onLog: () => {}, 202 | onComplete: () => { 203 | setTimeout(() => { 204 | let componentsHtmlTmp = fs.readFileSync( 205 | config.output + "toc.html", 206 | "utf8" 207 | ); 208 | t.truthy( 209 | componentsHtmlTmp.indexOf( 210 | '' 211 | ) > -1 212 | ); 213 | t.truthy( 214 | componentsHtmlTmp.indexOf( 215 | '' 216 | ) > -1 217 | ); 218 | t.truthy( 219 | componentsHtmlTmp.indexOf( 220 | '' 221 | ) > -1 222 | ); 223 | t.truthy( 224 | componentsHtmlTmp.indexOf( 225 | '
  • ' 226 | ) > -1 227 | ); 228 | 229 | t.end(); 230 | }, 1000); 231 | } 232 | }) 233 | ); 234 | }); 235 | -------------------------------------------------------------------------------- /test/render-pages/toc.md: -------------------------------------------------------------------------------- 1 | ### contents 2 | 3 | !{component1} 4 | !!{component2} 5 | ${component3} 6 | $${component4} 7 | -------------------------------------------------------------------------------- /test/utils.js: -------------------------------------------------------------------------------- 1 | const DM = require("../lib/index"); 2 | 3 | /** 4 | * Build Design Manual 5 | * and match log output to expected test result 6 | */ 7 | 8 | module.exports.buildAndMatchLogs = function(t, config, expected, cb) { 9 | const LOGGING = false; 10 | 11 | if (LOGGING && t) console.log(t.title); 12 | 13 | const log = []; 14 | DM.build( 15 | Object.assign(config, { 16 | onLog: msg => { 17 | log.push(msg); 18 | if (LOGGING) console.log(msg); 19 | }, 20 | onComplete: () => { 21 | if (LOGGING) console.log(""); 22 | 23 | if (typeof t !== "undefined" && expected) { 24 | expected = expected 25 | .split("\n") 26 | .map(str => str.trim()) 27 | .filter(str => Boolean(str.length)); 28 | 29 | t.deepEqual(expected.sort(), log.sort()); 30 | t.end(); 31 | } 32 | 33 | if (cb) cb(); 34 | } 35 | }) 36 | ); 37 | }; 38 | --------------------------------------------------------------------------------