├── .editorconfig
├── .gitdocs.json
├── .gitignore
├── .travis.yml
├── bin
├── compile
├── gitdocs
└── test
├── changelog.md
├── docs
├── .static
│ ├── logo-markdown.png
│ └── logo.svg
├── api
│ ├── commands.md
│ ├── config-file.md
│ └── front-matter.md
├── customizing-sidebar.md
├── getting-started.md
├── header-links.md
├── index-files.md
├── index.md
├── installation.md
├── json-data.md
├── production-builds.md
├── running-locally.md
├── static-files.md
├── syntax-highlighting.md
├── theming.md
├── troubleshooting
│ ├── faq.md
│ └── index.md
└── using-sources.md
├── license.md
├── notes.md
├── package.json
├── readme.md
├── src
├── cmds
│ ├── build.js
│ ├── help.js
│ ├── init.js
│ ├── manifest.js
│ ├── serve.js
│ └── version.js
├── core
│ ├── compiler.js
│ ├── compiler.spec.js
│ ├── components.js
│ ├── components.spec.js
│ ├── database.js
│ ├── database.spec.js
│ ├── filesystem.js
│ ├── filesystem.spec.js
│ ├── hydrate.js
│ ├── hydrate.spec.js
│ ├── index.js
│ ├── index.spec.js
│ ├── output.js
│ ├── output.spec.js
│ ├── robots.js
│ ├── robots.spec.js
│ ├── server.js
│ ├── server.spec.js
│ ├── sitemap.js
│ ├── sitemap.spec.js
│ ├── socket.js
│ ├── socket.spec.js
│ ├── source.js
│ ├── source.spec.js
│ ├── static.js
│ ├── static.spec.js
│ ├── syntax.js
│ ├── syntax.spec.js
│ ├── template.js
│ └── template.spec.js
├── index.js
├── sources
│ ├── git.js
│ └── local.js
└── utils
│ ├── arguments.js
│ ├── arguments.spec.js
│ ├── babel.js
│ ├── babel.spec.js
│ ├── config.js
│ ├── config.spec.js
│ ├── emit.js
│ ├── emit.spec.js
│ ├── frontmatter.js
│ ├── frontmatter.spec.js
│ ├── merge.js
│ ├── merge.spec.js
│ ├── path.js
│ ├── path.spec.js
│ ├── port.js
│ ├── port.spec.js
│ ├── promise.js
│ ├── promise.spec.js
│ ├── readline.js
│ ├── readline.spec.js
│ ├── system.js
│ ├── system.spec.js
│ ├── temp.js
│ ├── temp.spec.js
│ ├── theme.js
│ └── theme.spec.js
├── starter
├── about-us
│ ├── readme.md
│ └── work-with-us.md
├── getting-started
│ ├── installation.md
│ ├── readme.md
│ └── troubleshooting.md
└── readme.md
├── tests
├── help.test.js
├── helpers.js
├── index.test.js
├── manifest.test.js
└── mock
│ ├── .gitdocs.json
│ ├── external.md
│ ├── foo
│ ├── bar.md
│ ├── baz.md
│ └── index.md
│ ├── garply
│ ├── fred.md
│ ├── readme.md
│ └── waldo.md
│ ├── plugh.md
│ ├── qux
│ ├── corge.md
│ └── grault.md
│ ├── readme.md
│ ├── thud.md
│ └── xyzzy
│ ├── index.md
│ ├── zz.md
│ ├── zzz.md
│ └── zzzz.md
├── themes
├── default
│ ├── application
│ │ ├── index.js
│ │ └── styles.js
│ ├── breadcrumbs
│ │ ├── index.js
│ │ └── styles.js
│ ├── context.js
│ ├── header
│ │ ├── index.js
│ │ └── styles.js
│ ├── history.js
│ ├── icons
│ │ ├── close.js
│ │ ├── external.js
│ │ └── hamburger.js
│ ├── index.js
│ ├── loading
│ │ ├── index.js
│ │ └── styles.js
│ ├── logo
│ │ ├── index.js
│ │ └── styles.js
│ ├── markdown
│ │ ├── index.js
│ │ ├── overrides
│ │ │ ├── Code.js
│ │ │ ├── Header.js
│ │ │ └── Link.js
│ │ └── styles.js
│ ├── not-found
│ │ └── index.js
│ ├── page
│ │ ├── index.js
│ │ └── styles.js
│ ├── routes.js
│ ├── search
│ │ ├── db.js
│ │ ├── index.js
│ │ ├── strip.js
│ │ └── styles.js
│ ├── sidebar
│ │ ├── index.js
│ │ └── styles.js
│ ├── toc
│ │ ├── folder.js
│ │ ├── page.js
│ │ └── styles.js
│ └── utils
│ │ └── index.js
└── server.js
└── yarn.lock
/.editorconfig:
--------------------------------------------------------------------------------
1 | root = true
2 |
3 | [*]
4 | indent_style = space
5 | indent_size = 2
6 |
7 | end_of_line = lf
8 | charset = utf-8
9 | trim_trailing_whitespace = true
10 | insert_final_newline = true
11 |
12 | [*.md]
13 | trim_trailing_whitespace = false
14 |
--------------------------------------------------------------------------------
/.gitdocs.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "GitDocs",
3 | "root": "docs",
4 | "logo": "logo.svg",
5 | "languages": ["yaml"],
6 | "syntax": {
7 | "lineNumbers": false
8 | },
9 | "header_links": [
10 | {
11 | "title": "GitHub",
12 | "href": "https://github.com/timberio/gitdocs",
13 | "target": "_blank"
14 | }
15 | ]
16 | }
17 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | .DS_Store
2 | .vscode/
3 | .nyc_output/
4 | .tmp_build/
5 | .gitdocs_build/
6 | coverage/
7 | builds/
8 | node_modules/
9 | package-lock.json
10 | *.log
11 |
--------------------------------------------------------------------------------
/.travis.yml:
--------------------------------------------------------------------------------
1 | language: node_js
2 | sudo: false
3 | node_js:
4 | - "10"
5 | - "9"
6 | - "8"
7 | - "8.9" # minimum node version required
8 | after_success:
9 | - npm run coveralls
10 |
--------------------------------------------------------------------------------
/bin/compile:
--------------------------------------------------------------------------------
1 | #!/bin/bash
2 |
3 | set -e # exit when any command fails
4 |
5 | BASE_DIR=$(git rev-parse --show-toplevel)
6 | BIN_DIR="$BASE_DIR/node_modules/.bin"
7 | BUILDS_DIR="$BASE_DIR/builds"
8 | TMP_DIR="$BASE_DIR/.tmp_build"
9 |
10 | NODE_VERSION="node8.9.0"
11 | TARGETS=(
12 | "macos"
13 | "linux"
14 | "win"
15 | )
16 |
17 | PATHS=(
18 | "bin" "src" "starter" "themes"
19 | "package.json" "yarn.lock"
20 | )
21 |
22 | rm -rf $TMP_DIR $BUILDS_DIR
23 | mkdir $TMP_DIR
24 |
25 | for path in "${PATHS[@]}"
26 | do
27 | cp -r $BASE_DIR/$path $TMP_DIR
28 | done
29 |
30 | cd $TMP_DIR
31 | yarn install --production
32 |
33 | # temporary for linking timber ui
34 | # yarn link @timberio/ui
35 | # cd node_modules/@timberio/ui
36 | # rm -r node_modules && yarn install --production
37 | # cd $TMP_DIR
38 |
39 | for target in "${TARGETS[@]}"
40 | do
41 | ZIP="$BUILDS_DIR/gitdocs-$target.zip"
42 |
43 | if [ "$target" == "win" ]; then
44 | OUT=$BUILDS_DIR/$target.exe
45 | else
46 | OUT=$BUILDS_DIR/$target
47 | fi
48 |
49 | $BIN_DIR/pkg . \
50 | --targets $NODE_VERSION-$target \
51 | --output $OUT
52 |
53 | zip -j $ZIP $OUT.exe
54 | done
55 |
56 | rm -r $TMP_DIR
57 |
--------------------------------------------------------------------------------
/bin/gitdocs:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env node
2 |
3 | require('../src')()
4 |
--------------------------------------------------------------------------------
/bin/test:
--------------------------------------------------------------------------------
1 | #!/bin/bash
2 |
3 | mocha \
4 | --async-only \
5 | --reporter progress \
6 | --timeout 10000 \
7 | "$@"
8 |
--------------------------------------------------------------------------------
/changelog.md:
--------------------------------------------------------------------------------
1 | # Change Log
2 |
--------------------------------------------------------------------------------
/docs/.static/logo-markdown.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/vectordotdev/gitdocs/d9a11ab96041c94cbb216362d60e8e629b3aee2d/docs/.static/logo-markdown.png
--------------------------------------------------------------------------------
/docs/.static/logo.svg:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 | Group
5 | Created with Sketch.
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
--------------------------------------------------------------------------------
/docs/api/commands.md:
--------------------------------------------------------------------------------
1 | # Commands
2 |
3 | ### `gitdocs init`
4 |
5 | This initializes a new project. If you already have documentation files, you can specify the location of them when asked. If not, it will generate some example files for you to work from. It will also create a config file located at `.gitdocs.json` with some default values.
6 |
7 | See [Getting Started](/getting-started) for more info.
8 |
9 | ### `gitdocs serve` / `gitdocs start`
10 |
11 | This will start the local development environment. When the server is running, you can access your site at [http://localhost:8000](http://localhost:8000) (unless you changed the host or port.) It will live reload any files you are editing, meaning if you change some markdown, the page will instantly show the updated page as soon as you hit save.
12 |
13 | See [Running Locally](/running-locally) for more info.
14 |
15 | ### `gitdocs build`
16 |
17 | This will generate a static, minified, production-ready version of your documentation site. It will output to `.gitdocs_build` (or whatever your output folder is set to), and is ready to be uploaded to S3, GitHub Pages, Netlify, Surge or any other static hosting service.
18 |
19 | See [Production Builds](/production-builds) for more info.
20 |
--------------------------------------------------------------------------------
/docs/api/config-file.md:
--------------------------------------------------------------------------------
1 | # Config File
2 |
3 | A config file is generated when you run [`gitdocs init`](/api/commands), but it can also be created manually. It can be a JSON file or a Javascript file. Valid filenames are `.gitdocs.json` or `.gitdocs.js`.
4 |
5 | ### Using a Javascript file
6 |
7 | If you are using `.gitdocs.js` as your config file, you will need to export an object or a promise that resolves with an object.
8 |
9 | ```javascript
10 | module.exports = new Promise((resolve, reject) => {
11 | resolve({
12 | name: 'GitDocs',
13 | root: 'docs',
14 | })
15 | })
16 | ```
17 |
18 | ---
19 |
20 | | Key | Type | Default | Description |
21 | | --- | ---- | ------- | ----------- |
22 | | name | string | | The name of your documentation site. _Used as the logo when no file is specified._ |
23 | | root | string | `.` | The root directory for your documentation. |
24 | | output | string | `.gitdocs_build` | The directory where the static site is outputted to. |
25 | | static | string | `.static` | The directory to use for static assets. |
26 | | temp | string | `[SYSTEM]` | The directory to use for temporary files. |
27 | | logo | string | | The location of a logo image file, relative to the [static directory](/static-files). |
28 | | baseURL | string | `/` | The base URL to use when generating the routes. |
29 | | domain | string | | The domain name that gets prepended to URLs in the sitemap. |
30 | | crawlable | boolean | `true` | Whether your site should be crawlable by search engines. |
31 | | host | string | `localhost` | The hostname to use for the local server. |
32 | | port | number | `8000` | The port to use for the local server. |
33 | | languages | array | `['bash', 'json']` | The languages to include for [syntax highlighting](/syntax-highlighting). |
34 | | header_links | array | `[]` | External links to include in the header. See [header links](/header-links) for more details. |
35 | | theme | string | `default` | The name of the theme to use. There is only one theme at the moment. |
36 | | breadcrumbs | boolean | `true` | Whether to enable breadcrumb links on each page. This can also be disabled [in the front matter](/api/front-matter). |
37 | | prefix_titles | boolean | `false` | If enabled, will automatically generate an `h1` tag on each page. |
38 | | table\_of_contents | object | | |
39 | | table\_of_contents.page | boolean | `true` | Whether to enable the table of contents for headers in the page. |
40 | | table\_of_contents.folder | boolean | `true` | Whether to enable the table of contents for pages in a folder (shown in the [index page](/index-files).) |
41 | | syntax | object | | |
42 | | syntax.theme | string | `atom-one-light` | The [syntax highlighting theme](/syntax-highlighting/#choosing-a-style) to use. |
43 | | syntax.renderer | string | `hljs` | The [syntax highlighting renderer](/syntax-highlighting/#choosing-a-renderer) to use. Options are `hljs` or `prism`. |
44 | | syntax.lineNumbers | boolean | `true` | Whether to show line numbers in code blocks. |
45 |
--------------------------------------------------------------------------------
/docs/api/front-matter.md:
--------------------------------------------------------------------------------
1 | # Front Matter
2 |
3 | Every markdown file can include front matter for defining metadata. All front matter is optional, and in most cases, can be defined on a site-wide level by using the [config file](/api/config-file) instead.
4 |
5 | You can also define front matter in the `items` array of the parent index page ([see here](/customizing-sidebar)). When using this method, it's important to note that data from the parent item will always take priority over front matter in the file itself.
6 |
7 | ---
8 |
9 | | Key | Type | Default | Description |
10 | | --- | ---- | ------- | ----------- |
11 | | title | string | `[FILENAME]` | The page title to use in the sidebar, as well as the browser tab. |
12 | | description | string | | The description used when generating a [table of contents](/index-files/#table-of-contents). |
13 | | url | string | | Defines a [permalink](https://en.wikipedia.org/wiki/Permalink) for the page. Meaning if you change the location of the file, it will always keep the same URL. |
14 | | draft | boolean | `false` | If a page is in draft mode, it will show up when running locally but will not be included in the production build. |
15 | | hidden | boolean | `false` | If a page is hidden, it will build as normal but not show up in the sidebar. |
16 | | tags | array | `[]` | A comma-seperated list of tags for the page. |
17 | | related | list | `[]` | A list of related docs, must use the relate url. |
18 | | breadcrumbs | boolean | `true` | Whether to display breadcrumb links on the page. |
19 | | table\_of_contents | object | | Follows the same format as the [config file](/api/config-file), but will only toggle the table of contents for the current page. |
20 | | source | string | | See [using sources](/using-sources). |
21 | | source_type | string | `local` | See [using sources](/using-sources). |
22 | | source_root | string | `docs` | See [using sources](/using-sources). |
23 | | source_branch | string | `master` | See [using sources](/using-sources). |
24 | | items | array | | See [customizing sidebar](/customizing-sidebar). Will replace all children items. |
25 | | items_prepend | array | | See [customizing sidebar](/customizing-sidebar). Will merge items to beginning of list. |
26 | | items_append | array | | See [customizing sidebar](/customizing-sidebar). Will merge items to end of list. |
27 |
--------------------------------------------------------------------------------
/docs/customizing-sidebar.md:
--------------------------------------------------------------------------------
1 | ---
2 | description: How to customize the link order and titles of items in the sidebar.
3 | ---
4 | # Customizing the Sidebar
5 |
6 | At times you may want to customize how the sidebar looks and add additional items to it--whether it's external links, dividers or changing the link order. This can all be done in the front matter of an [index file](/index-files), using the `items` key which is a list of the current item's children.
7 |
8 | ```yaml
9 | ---
10 | items:
11 | - path: foo.md
12 | - path: bar.md
13 | - path: baz.md
14 |
15 | # or shorthand
16 | items:
17 | - foo.md
18 | - bar.md
19 | - baz.md
20 | ---
21 | ```
22 |
23 | It's important to note that defining `items` will overwrite the default children. If you don't manually list all of your files, they will not be included. This can be useful if you want to manually exclude some pages, but may be cumbersome if you have many files.
24 |
25 | If you only want to change _a few_ pages and avoid redefining the entire file tree, you can use `items_prepend` or `items_append`. These follow the same structure as `items` but instead of replacing the children, GitDocs will merge the items at the beginning or end of the list.
26 |
27 | ### No Index? No Problem.
28 |
29 | How can you do any of this if there is no index file in a folder? Easy enough. You can nest the `items` key as deeply as you'd like. So if a folder doesn't have an index file, you can define the order in the nearest index file like so:
30 |
31 | ```yaml
32 | items:
33 | - foo.md
34 | - path: bar
35 | items:
36 | - qux.md
37 | - baz.md
38 | ```
39 |
40 | ### Defining front matter for children
41 |
42 | You can define any valid [front matter value](/api/front-matter) in the item, just as you would in the file itself. GitDocs will automatically merge a file's front matter with data from the parent (which will take priority.) For example, if you want to change the title of a page, these two options will produce the same result:
43 |
44 | ```yaml
45 | # foo/bar.md
46 | ---
47 | title: BAR
48 | ---
49 | ```
50 |
51 | ```yaml
52 | # foo/index.md
53 | ---
54 | items:
55 | - path: bar.md
56 | title: BAR
57 | ---
58 | ```
59 |
60 | ## Changing the Order
61 |
62 | Following the structure explained above, changing the order of sidebar links is as simple defining `items`/`items_prepend`/`items_append` and listing the children in the order you'd like. GitDocs will always respect the order of your list.
63 |
64 | ## External Links
65 |
66 | You can specify an external link by using `url` instead of `path`. These look the same as regular links in the sidebar, but will have an icon next to them and open in a new tab.
67 |
68 | ```yaml
69 | ---
70 | items:
71 | - url: https://github.com/timberio/gitdocs
72 | title: GitHub Repo
73 | ---
74 | ```
75 |
76 | ## Components
77 |
78 | You can specify a component in place of a link by using `component` instead of `path`. This can be useful to visually seperate items in your sidebar.
79 |
80 | ```yaml
81 | ---
82 | items:
83 | - foo.md
84 | - component: Divider
85 | - bar.md
86 | ---
87 | ```
88 |
89 | Available components:
90 |
91 | * `Divider`
92 |
93 | ## Hidden Pages
94 |
95 | If a page is not ready to be published or you simply want to omit the link from the sidebar, you have a few options:
96 |
97 | * `draft: true` in the front matter will include the page normally when you run the [local server](/running-locally), but _not_ when you [build for production](/production-builds).
98 | * `hidden: true` in the front matter will include the page in production and the URL will be publicly accessible, but there will be no link in the sidebar.
99 | * Define `items` in the front matter and intentionally leave out your hidden pages.
100 |
101 | ---
102 |
103 |
106 |
--------------------------------------------------------------------------------
/docs/getting-started.md:
--------------------------------------------------------------------------------
1 | ---
2 | description: How to initialize the example files and build your first documentation site.
3 | ---
4 | # Getting Started
5 |
6 | GitDocs is meant to live unobtrusively alongside your source code. If you don't already have documentation, we will create some example files for you. If you do already have documentation, just point to the correct folder when asked and you will be ready to go! Once you have GitDocs [installed](/installation), run these commands:
7 |
8 | ```bash
9 | $ mkdir my-project # if you're starting fresh with a new project
10 | $ cd my-project
11 | ```
12 |
13 | ```bash
14 | $ gitdocs init
15 |
16 | ❯ What is the name of your project? (my-project)
17 | ❯ Do you already have some docs? (y/n) n
18 |
19 | ❯ Created new config file at .gitdocs.json
20 | ❯ Creating some documentation at docs/
21 | ❯ Ready to go! Run gitdocs serve to get started
22 | ```
23 |
24 | ## Routing and File Structure
25 |
26 | The filesystem will always be the source of truth for routes defined in your site. For example, if you want a page located at `mydocs.com/foo/bar`, your docs will need to have a `docs/foo/bar.md` file. This becomes more apparent when you start using [sources](/using-sources). The only exception to this is if you define `url` in the [front matter](/api/front-matter), which will create a permalink.
27 |
28 | ---
29 |
30 |
33 |
--------------------------------------------------------------------------------
/docs/header-links.md:
--------------------------------------------------------------------------------
1 | ---
2 | description: How to define external links in the header next to the search bar.
3 | ---
4 | # Header Links
5 |
6 | There is space on the right side of the search bar for some external links, if you'd rather not put them in the sidebar. These links are easy to define by adding a `header_links` array to the [config file](/api/config-file). You need to include a `title` key, then any other properties you define will get spread onto the `` tag (if you want to open in a new tab, for example.)
7 |
8 | ```json
9 | {
10 | "header_links": [
11 | {
12 | "title": "GitHub",
13 | "href": "https://github.com/timberio/gitdocs",
14 | "target": "_blank"
15 | }
16 | ]
17 | }
18 | ```
19 |
20 | You can define as many links as you'd like here, but be careful that you don't add too many as it will break the design and responsiveness of the site. If you have many external links, only add the best ones to the header and put the rest [in the sidebar](/customizing-sidebar/#external-links).
21 |
22 | ---
23 |
24 |
27 |
--------------------------------------------------------------------------------
/docs/index-files.md:
--------------------------------------------------------------------------------
1 | ---
2 | description: The how and when of utilizing index files in your docs site.
3 | ---
4 | # Index Files
5 |
6 | Every folder can have an index file that will show when the top-level link is clicked. GitDocs will look for a valid index filename in each folder. If one doesn't exist, clicking the top-level link in the sidebar will simply expand the list without navigating to a new route (see the API section of this site, it doesn't have an index.)
7 |
8 | Index files are totally optional with the exception of a root index.
9 |
10 | ## Root Index
11 |
12 | Every website needs a home page. GitDocs requires at least one index file at the root of your documentation to use as the home page. If you are unsure what to put here, a simple title and description will be sufficient. We will add a table of contents to the bottom of the page, which should fill up the rest of the space nicely.
13 |
14 | ## Valid Index Filenames
15 |
16 | * `index.md`
17 | * `readme.md`
18 |
19 | Index filenames are case insensitive, so using `README.md` will still work.
20 |
21 | ## Table of Contents
22 |
23 | If you have an index file in a folder, a table of contents will automatically be generated and appended to the content using the `title` and `description` of each item. This can be disabled for the entire site in the [config file](/api/config-file), or for individual folders in the [front matter](/api/front-matter) of the index file.
24 |
25 | _Note: An item will not show up in the folder's table of contents if it's a folder without an index._
26 |
27 | ---
28 |
29 |
32 |
--------------------------------------------------------------------------------
/docs/index.md:
--------------------------------------------------------------------------------
1 | ---
2 | table_of_contents:
3 | page: false
4 | items:
5 | - installation.md
6 | - getting-started.md
7 | - running-locally.md
8 | - production-builds.md
9 | - static-files.md
10 | - using-sources.md
11 | - index-files.md
12 | - customizing-sidebar.md
13 | - header-links.md
14 | - syntax-highlighting.md
15 | - theming.md
16 | - json-data.md
17 | - component: Divider
18 | - troubleshooting
19 | - path: api
20 | title: API
21 | items:
22 | - front-matter.md
23 | - config-file.md
24 | - commands.md
25 | ---
26 | # Welcome to GitDocs
27 |
28 | GitDocs helps you create beautiful, SEO-friendly documentation sites from markdown files that live alongside your source code. Cross-compile from multiple git repos and run locally for a great publishing experience.
29 |
--------------------------------------------------------------------------------
/docs/installation.md:
--------------------------------------------------------------------------------
1 | ---
2 | description: Installing GitDocs onto your machine is a simple process using NPM.
3 | ---
4 | # Installation
5 |
6 | We currently publish GitDocs as an NPM package. After [installing Node and NPM](https://www.npmjs.com/get-npm) (we require Node v8.9 or greater), run the following to install the GitDocs beta as a global:
7 |
8 | ```bash
9 | $ npm install -g gitdocs@next
10 | ```
11 |
12 | You can now use the `gitdocs` command in your terminal. Running without any commands will show the following help menu:
13 |
14 | ```
15 | Usage
16 |
17 | gitdocs [options]
18 |
19 | for further info about a command:
20 | gitdocs --help or gitdocs help
21 |
22 | Commands
23 |
24 | init .................... initialize a new project
25 | serve ................... runs the local development server
26 | build ................... creates a static production bundle
27 | help .................... show the help menu for a command
28 |
29 | Options
30 |
31 | --config, -c ............ customize the config file location
32 | --help, -h .............. display the usage menu for a command
33 | --version, -v ........... show the version number
34 | ```
35 |
36 | ---
37 |
38 |
41 |
--------------------------------------------------------------------------------
/docs/json-data.md:
--------------------------------------------------------------------------------
1 | ---
2 | title: JSON Data
3 | description: Query the raw JSON data for your documentation to use in other apps.
4 | ---
5 | # JSON Data
6 |
7 | It can often be useful to have access to your raw documentation data for the purpose of searching, embedding, etc. This feature comes out of the box with GitDocs as raw JSON files for each page. When you build your site, a JSON file is generated alongside each HTML file containing the meta data and page contents, along with an overall database file for the site.
8 |
9 | ### index.json
10 |
11 | When your site is built, you get an `index.html` and `index.json` for every page. The HTML file is used by web browsers when you go to a URL (e.g. `mydocs.com/foo/bar/` will use `/foo/bar/index.html`), but you can also make a request to `mydocs.com/foo/bar/index.json` for the raw data.
12 |
13 | _Note: These files will only exist when you [build for production](/production-builds), not when running the local server._
14 |
15 | ### db.json
16 |
17 | Your build will also include a main database file, containing content for your entire site as well as breadcrumbs, tags, etc. This is how the GitDocs search works--by making a network request for this file and statically searching titles and content.
18 |
19 | This file is basically a concatenation of all the `index.json` files, and can be found at the root of your site.
20 |
--------------------------------------------------------------------------------
/docs/production-builds.md:
--------------------------------------------------------------------------------
1 | ---
2 | description: Generate a minified, production-ready static documentation website.
3 | ---
4 | ## Production Builds
5 |
6 | When you are ready to deploy your site to the internets, you'll want to bundle and minify everything. We do all the dirty work behind the scenes for you, so all you need to do is run a single command:
7 |
8 | ```bash
9 | $ gitdocs build
10 | ```
11 |
12 | Once the process completes, you will have a static site located at `.gitdocs_build/`. This is ready to be deployed to S3, GitHub Pages, Netlify, etc.
13 |
14 | ### robots.txt and sitemap.xml
15 |
16 | Both of these files are created for you automagically and are available at /robots.txt and /sitemap.xml . You can change whether or not your robots.txt file allows your site to be crawled by setting `crawlable: false` in the [config file](/api/config-file).
17 |
18 | ---
19 |
20 |
23 |
--------------------------------------------------------------------------------
/docs/running-locally.md:
--------------------------------------------------------------------------------
1 | ---
2 | description: Get a live preview of changes you make by running the local server.
3 | ---
4 | # Running Locally
5 |
6 | To get a live preview of your docs as you write them, you can run our local development server. While in the root directory of your project (where the `.gitdocs.json` lives), simply run this:
7 |
8 | ```bash
9 | $ gitdocs serve
10 | ```
11 |
12 | You can now access your documentation at http://localhost:8000 . How nice is that? Try changing some text in one of the markdown files and watch it instantly live reload in your browser.
13 |
14 | ## Live Reloading
15 |
16 | When editing your docs, the pages will automatically live reload in your browser when you hit save. Usually faster than it takes for you to swich windows. This provides a seamless way to view your rendered markdown without the hassle of using a markdown editor.
17 |
18 | _Note: Live reload is currently limited to the markdown content itself. Any changes to the file structures, front matter or config file require the server to be restarted. [This is an open issue.](https://github.com/timberio/gitdocs/issues/139)_
19 |
20 | ---
21 |
22 |
25 |
--------------------------------------------------------------------------------
/docs/static-files.md:
--------------------------------------------------------------------------------
1 | ---
2 | description: How to include and use static files (such as images) in your build.
3 | ---
4 | # Static Files
5 |
6 | There is a special directory reserved in your root folder for static files: `.static/`. Any files you put in here will be served over the local server or copied over to the output directory when you build your site. For example, a file located at `docs/.static/logo.png` will be available in your documentation site at `/logo.png`. You can change the name of your static folder in the [config file](/api/config-file/#static).
7 |
8 | _This folder is relative to your docs folder in your project. If your docs are located at `docs/`, your static folder should be at `docs/.static`._
9 |
10 | ## Your Site Logo
11 |
12 | The logo is a special static asset that gets used in the GitDocs theme (as you can see in the top left of this site.) It will default to the name of your site, but you can specify a [custom logo file](/api/config-file/#logo) in the config. This file should be available in your static folder.
13 |
14 | ---
15 |
16 |
19 |
--------------------------------------------------------------------------------
/docs/syntax-highlighting.md:
--------------------------------------------------------------------------------
1 | ---
2 | description: Configuring custom syntax highlighting to suit your needs.
3 | ---
4 | # Syntax Highlighting
5 |
6 | GitDocs supports syntax highlighting for 176 different languages out of the box. The only thing you need to do is specify which languages you're using in the config file (since it would generate large bundle files if we included everything!) `bash` and `json` are enabled by default, but it's easy to enable other languages [in the config file](/api/config-file).
7 |
8 | ## Setting a Language
9 |
10 | To set the language for a code block, add the language name after the triple backticks like this:
11 |
12 | ```
13 | ```javascript
14 | console.log("Hello World!")
15 | ```
16 | ```
17 |
18 | ## Choosing a Renderer
19 |
20 | Both [HighlightJS](https://highlightjs.org) and [PrismJS](https://prismjs.com) are supported, since language support is slightly different between them (mainly PrismJS supports JSX while HighlightJS does not.) GitDocs defaults to HighlightJS, but you can enable PrismJS by [changing the renderer](/api/config-file/#syntax-renderer) in the config file.
21 |
22 | You can also use any syntax style/theme supported by the renderer you are using.
23 |
24 | * [HighlightJS Styles](https://github.com/isagalaev/highlight.js/tree/master/src/styles)
25 | * [PrismJS Styles](https://github.com/PrismJS/prism-themes)
26 |
27 | ## Line Numbers
28 |
29 | Line numbers for all syntax blocks are enabled by default. They can be disabled in the [config file](/api/config-file/#syntax-lineNumbers).
30 |
31 | ---
32 |
33 |
36 |
--------------------------------------------------------------------------------
/docs/theming.md:
--------------------------------------------------------------------------------
1 | ---
2 | description: Use our built-in default theme, customize it, or use your own theme.
3 | ---
4 | # Theming
5 |
6 | You cannot customize the GitDocs theme at this time. This feature is currently [under development](https://github.com/timberio/gitdocs/issues/83).
7 |
8 | ---
9 |
10 |
13 |
--------------------------------------------------------------------------------
/docs/troubleshooting/faq.md:
--------------------------------------------------------------------------------
1 | ---
2 | title: FAQ
3 | description: Frequently Asked Questions about the GitDocs app and related products.
4 | ---
5 |
--------------------------------------------------------------------------------
/docs/troubleshooting/index.md:
--------------------------------------------------------------------------------
1 | ---
2 | description: Having trouble getting started? Find help in our FAQs or reach out.
3 | ---
4 | # Troubleshooting
5 |
6 | Are you having problems with GitDocs? Did you find a strange bug? Think of an amazing feature that would make your life better? It is currently maintained by the good folks over at [Timber](https://timber.io), so feel free to reach out to us or open an issue in the [GitHub repo](https://github.com/timberio/gitdocs/issues).
7 |
--------------------------------------------------------------------------------
/docs/using-sources.md:
--------------------------------------------------------------------------------
1 | ---
2 | description: Pull in documentation from outside sources, such as another git repo.
3 | ---
4 | # Using Sources
5 |
6 | GitDocs has the awesome ability to compose a documentation site from files spread across multiple locations. A source can be a local file, another git repo, or a remote file (support pending.) To use a source, define `source` and `source_type` in the front matter. Once we fetch the source, it will _replace_ the original file that defines the source. For example, let's say our documentation structure looks something like this:
7 |
8 | ```
9 | docs/
10 | languages/
11 | python/
12 | ruby/
13 | node.md
14 | ```
15 |
16 | ```yaml
17 | ---
18 | # in docs/languages/node.md
19 | source: https://github.com/my-org/node.git
20 | source_type: git
21 | ---
22 | ```
23 |
24 | and `https://github.com/my-org/node` looks something like this:
25 |
26 | ```
27 | docs/
28 | installation/
29 | readme.md
30 | troubleshooting.md
31 | src/
32 | package.json
33 | ```
34 |
35 | What will happen when we build this site? GitDocs will clone the Node repo behind the scenes, and replace `docs/languages/node.md` with the contents of the repo's `docs` folder. The result will look something like this:
36 |
37 | ```
38 | docs/
39 | languages/
40 | python/
41 | ruby/
42 | node/
43 | installation/
44 | readme.md
45 | troubleshooting.md
46 | ```
47 |
48 | See how the `node.md` file was replaced by the contents of `node.git/docs/`? You can think of the file as a placeholder for the contents of the source.
49 |
50 | ## Local Source
51 |
52 | You can point to another file in the filesystem and use it as a replacement for the current file.
53 |
54 | ```yaml
55 | ---
56 | source: /a/far/off/file.md
57 | source_type: local
58 | ---
59 | ```
60 |
61 | ## Git Source
62 |
63 | Using a remote git repo is as easy as pointing to the repo URL (must be public) and defining a branch and root directory where the docs live.
64 |
65 | ```yaml
66 | ---
67 | source: https://github.com/my-org/my-repo.git
68 | source_type: git
69 | source_branch: master # this is the default
70 | source_root: docs # this is the default
71 | ---
72 | ```
73 |
74 | ## Remote Source
75 |
76 | Support for remote source files is currently pending.
77 |
78 | ## Best Practices
79 |
80 | Since external sources cannot be live reloaded, it's best to work on each repo's documentation as a self-contained documentation site and merge into the "main" repo when it is ready to go. This lets you take advantage of the [dev environment](/running-locally).
81 |
82 | ---
83 |
84 |
87 |
--------------------------------------------------------------------------------
/license.md:
--------------------------------------------------------------------------------
1 | # License
2 |
3 | Copyright (c) 2018, Timber Technologies, Inc.
4 |
5 | Permission to use, copy, modify, and/or distribute this software for any purpose
6 | with or without fee is hereby granted, provided that the above copyright notice
7 | and this permission notice appear in all copies.
8 |
9 | THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES WITH
10 | REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY AND
11 | FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY SPECIAL, DIRECT,
12 | INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM LOSS
13 | OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR OTHER
14 | TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR PERFORMANCE OF
15 | THIS SOFTWARE.
16 |
--------------------------------------------------------------------------------
/notes.md:
--------------------------------------------------------------------------------
1 | "Git based documentation & knowledge base."
2 |
3 | ### Worth checking out
4 |
5 | * https://github.com/SBoudrias/Inquirer.js
6 | * https://github.com/tj/commander.js/
7 | * https://github.com/markdown-it/markdown-it#syntax-extensions
8 | * https://github.com/nuxt/webpackbar
9 |
10 | ### Static site generators:
11 | * https://github.com/phenomic/phenomic
12 | * https://github.com/jaredpalmer/razzle
13 | * https://github.com/markdalgleish/static-site-generator-webpack-plugin
14 | * https://serverless.com/framework/docs/
15 | * https://ticky.github.io/markdown-component-loader/
16 | * https://github.com/static-dev/spike
17 | * https://serverless.com/framework/docs/
18 | * https://github.com/kentor/tiny-ssg
19 | * https://antwar.js.org
20 | * https://github.com/cuttlebelle/cuttlebelle
21 | * https://cuttlebelle.com/documentation
22 | * https://github.com/c8r/x0
23 |
24 | ### Inspiration
25 |
26 | * https://github.com/QingWei-Li/docsify-cli/blob/master/bin/docsify
27 | * https://docute.js.org
28 | * https://github.com/rhiokim/flybook
29 | * https://tapdaq.com/docs
30 | * https://amplitude.github.io/redux-query/#/mounting (look at repl)
31 | * https://developer.paciellogroup.com/blog/2017/09/infusion-an-inclusive-documentation-builder/
32 | * https://docs.resin.io/raspberrypi3/nodejs/getting-started/#introduction
33 | * https://twitter.com/mjackson/status/953818596211294208
34 | * https://developer.kayako.com/api/v1/users/activities/
35 | * https://docusaurus.io/docs/en/api-pages.html
36 | * https://docs.smooch.io/changelog/
37 | * https://help.helpjuice.com/Internal-Knowledge-Bases/can-i-hide-some-categories-questions-from-public
38 | * https://stripe.com/atlas/guides
39 | * https://javascript.info/callbacks?map (tree search view)
40 | * https://stripe.com/docs
41 | * https://www.prisma.io/docs/reference/
42 | * https://docs.bugsnag.com/
43 | * https://zeit.co/docs
44 |
45 | >>>>>>> master
46 |
47 | ### Resources
48 |
49 | * https://developer.atlassian.com/blog/2015/11/scripting-with-node/
50 | * https://sthom.kiwi/posts/react-in-markdown-updated/
51 |
52 | ### React + Markdown
53 |
54 | * https://github.com/gatsbyjs/gatsby/issues/312#issuecomment-336681894
55 | * https://www.npmjs.com/package/reactdown
56 | * https://github.com/gatsbyjs/gatsby/pull/3012
57 | * http://md-to-react.surge.sh/
58 | * https://github.com/fazouane-marouane/remark-jsx/tree/master/packages/remark-custom-element-to-hast
59 | * https://github.com/cerebral/marksy
60 | * https://andreypopp.github.io/reactdown/
61 | * https://github.com/rexxars/react-markdown (keep an eye on this)
62 | * https://github.com/phenomic/phenomic/search?utf8=%E2%9C%93&q=remark
63 | * https://github.com/probablyup/markdown-to-jsx/blob/master/README.md#optionsoverrides---rendering-arbitrary-react-components
64 |
65 | ### Ideas
66 |
67 | * components for data visualization
68 | * runkit/codepen/codesandbox/webpackbin embeds
69 | * github pages mode (just react app)
70 | * configurable sidebar side (left|right)
71 | * mermaid integration https://mermaidjs.github.io/ (https://github.com/knsv/mermaid)
72 | * lucidchart integration
73 | * props tables + definitions
74 | * custom react/vue/etc components
75 | * analytics hooks
76 | * search built in
77 | * front matter for seo options and config
78 | * layout options
79 | * optional home page
80 | * ascii cinema plugin
81 | * custom plugin system
82 | * link to specific line in code (ex. #section-name-L22)
83 | * Next/Last page https://cl.ly/3n3g3Q2t2L1l
84 | * language selection (intl)
85 | * version selection
86 | * custom styling for a changelog.md
87 | * overview page like https://timber.io/docs
88 | * feedback area https://cl.ly/1h0X1c3b1Q1Z, might require a hosted plan
89 | * potentially have a special API section that parses methods like https://doc.esdoc.org/github.com/jy95/torrent-files-library/typedef/index.html#static-typedef-customParsingFunction
90 |
91 | ### Components
92 |
93 | * steps
94 | * code demos
95 | * editor
96 | * embeds
97 | * charts
98 | * diagrams
99 | * graphs
100 |
101 | ### Jekyll Insp.
102 |
103 | * https://github.com/poole/poole
104 |
105 | ### Issues
106 |
107 | * Syntaxes get passed through config object properly
108 | * Refractor includes hastscript which includes camelcase, that breaks in uglify
109 | * Marksy code override needs tracker context
110 |
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "gitdocs",
3 | "version": "2.0.0-beta23",
4 | "description": "Easy to use, SEO-friendly, beautiful documentation that lives in your git repo.",
5 | "license": "MIT",
6 | "homepage": "https://github.com/timberio/gitdocs#readme",
7 | "repository": {
8 | "type": "git",
9 | "url": "git+https://github.com/timberio/gitdocs.git"
10 | },
11 | "engines": {
12 | "node": ">=8.9"
13 | },
14 | "keywords": [
15 | "documentation",
16 | "static",
17 | "markdown",
18 | "react",
19 | "docs"
20 | ],
21 | "files": [
22 | "bin/",
23 | "src/",
24 | "starter/",
25 | "themes/",
26 | "license.md",
27 | "readme.md"
28 | ],
29 | "bin": {
30 | "gitdocs": "bin/gitdocs"
31 | },
32 | "scripts": {
33 | "test:lint": "eslint src tests themes",
34 | "test:unit": "./bin/test 'src/**/*.spec.js'",
35 | "test:integration": "./bin/test 'tests/**/*.test.js'",
36 | "test": "nyc npm-run-all test:*",
37 | "posttest": "nyc report -r=lcov",
38 | "coveralls": "cat coverage/lcov.info | coveralls",
39 | "compile": "./bin/compile"
40 | },
41 | "devDependencies": {
42 | "code": "^5.2.0",
43 | "coveralls": "^3.0.1",
44 | "eslint": "^4.19.1",
45 | "eslint-config-timber": "^1.0.22",
46 | "execa": "^0.10.0",
47 | "mocha": "^5.2.0",
48 | "mock-fs": "^4.5.0",
49 | "nexe": "^2.0.0-rc.28",
50 | "npm-run-all": "^4.1.3",
51 | "nyc": "^11.8.0",
52 | "pkg": "^4.3.1",
53 | "sinon": "^5.0.10"
54 | },
55 | "dependencies": {
56 | "@timberio/ui": "^2.0.1",
57 | "axios": "^0.18.0",
58 | "babel-core": "^6.26.3",
59 | "babel-loader": "^7.1.4",
60 | "babel-plugin-transform-class-properties": "^6.24.1",
61 | "babel-plugin-transform-object-rest-spread": "^6.26.0",
62 | "babel-plugin-transform-runtime": "^6.23.0",
63 | "babel-preset-env": "^1.7.0",
64 | "babel-preset-react": "^6.24.1",
65 | "babel-register": "^6.26.0",
66 | "chalk": "^2.4.1",
67 | "chokidar": "^2.0.3",
68 | "connect": "^3.6.6",
69 | "deepmerge": "^2.1.0",
70 | "emotion": "^9.1.3",
71 | "emotion-server": "^9.1.3",
72 | "exit-hook": "^2.0.0",
73 | "file-loader": "^1.1.11",
74 | "fs-extra": "^6.0.1",
75 | "gray-matter": "^4.0.1",
76 | "history": "^4.7.2",
77 | "html-minifier": "^3.5.16",
78 | "js-search": "^1.4.2",
79 | "markdown-to-jsx": "6.6.7",
80 | "markdown-toc": "^1.2.0",
81 | "minimist": "^1.2.0",
82 | "ncp": "^2.0.0",
83 | "progress": "^2.0.0",
84 | "prop-types": "^15.6.1",
85 | "raf": "^3.4.0",
86 | "react": "^16.4.0",
87 | "react-click-outside": "^3.0.1",
88 | "react-content-loader": "^3.1.2",
89 | "react-dom": "^16.4.0",
90 | "react-emotion": "^9.1.3",
91 | "react-feather": "^1.1.0",
92 | "react-helmet": "^5.2.0",
93 | "react-highlight-words": "^0.11.0",
94 | "react-router-dom": "^4.2.2",
95 | "react-syntax-highlighter": "^7.0.4",
96 | "serve-static": "^1.13.2",
97 | "simple-git": "^1.95.0",
98 | "tmp": "^0.0.33",
99 | "webpack": "^4.8.3",
100 | "webpack-dev-middleware": "^3.1.3",
101 | "webpack-hot-middleware": "^2.22.2",
102 | "ws": "^5.2.0"
103 | },
104 | "eslintConfig": {
105 | "extends": "timber",
106 | "globals": {
107 | "describe": true,
108 | "it": true,
109 | "beforeEach": true,
110 | "afterEach": true
111 | },
112 | "rules": {
113 | "react/no-did-mount-set-state": 0
114 | }
115 | },
116 | "pkg": {
117 | "scripts": [
118 | "src/**/*.js"
119 | ],
120 | "assets": [
121 | "starter/**/*",
122 | "themes/**/*",
123 | "node_modules/**/*"
124 | ]
125 | },
126 | "nyc": {
127 | "all": true,
128 | "include": [
129 | "src"
130 | ],
131 | "exclude": [
132 | "**/*.spec.js"
133 | ]
134 | }
135 | }
136 |
--------------------------------------------------------------------------------
/readme.md:
--------------------------------------------------------------------------------
1 | 
2 |
3 | GitDocs helps you create beautiful, SEO-friendly documentation sites from markdown files that live alongside your source code. Cross-compile from multiple git repos and run locally for a great publishing experience.
4 |
5 | ## Features
6 |
7 | - Syntax highlighting out of the box.
8 | - Fast static searching of all your docs.
9 | - Cross-compile from multiple git repos.
10 | - Static JSON data for every page.
11 | - Automatically generated table of contents.
12 | - SEO-friendly--everything is static!
13 |
14 | ## Example Sites
15 |
16 | - https://gitdocs.netlify.com
17 | - https://docs.timber.io
18 | - https://docs.dev.to
19 |
20 |
21 |
22 |
23 |
24 | 
25 |
26 |
34 |
--------------------------------------------------------------------------------
/src/cmds/build.js:
--------------------------------------------------------------------------------
1 | const fs = require('fs-extra')
2 | const core = require('../core')
3 | const output = require('../core/output')
4 | const { buildBundle } = require('../core/compiler')
5 | const { styles, log } = require('../utils/emit')
6 |
7 | module.exports = async (args, config) => {
8 | log('Creating your documentation site', true)
9 |
10 | log('Bundling the Javascript app')
11 | await fs.emptyDir(config.output)
12 |
13 | const {
14 | props,
15 | compiler,
16 | } = await core('production', config)
17 |
18 | const {
19 | entrypoints,
20 | } = await buildBundle(compiler)
21 |
22 | log('Rendering and outputting HTML pages')
23 |
24 | await output(entrypoints, props)
25 | log(`Site has been saved to ${styles.note(`${config.output}/`)}`)
26 | }
27 |
28 | module.exports.menu = `
29 | ${styles.title('Usage')}
30 |
31 | gitdocs build [options]
32 |
33 | ${styles.title('Options')}
34 |
35 | ${styles.subnote('no options yet')}`
36 |
--------------------------------------------------------------------------------
/src/cmds/help.js:
--------------------------------------------------------------------------------
1 | const { styles, chars } = require('../utils/emit')
2 |
3 | const mainMenu = `
4 | ${chars.LOGO}
5 |
6 | ${styles.title('Usage')}
7 |
8 | gitdocs [options]
9 |
10 | ${styles.subnote('for further info about a command:')}
11 | gitdocs --help ${styles.subnote('or')} gitdocs help
12 |
13 | ${styles.title('Commands')}
14 |
15 | init ${styles.inactive('....................')} initialize a new project
16 | serve ${styles.inactive('...................')} runs the local development server
17 | build ${styles.inactive('...................')} creates a static production bundle
18 | help ${styles.inactive('....................')} show the help menu for a command
19 |
20 | ${styles.title('Options')}
21 |
22 | --config, -c ${styles.inactive('............')} customize the config file location
23 | --help, -h ${styles.inactive('..............')} display the usage menu for a command
24 | --version, -v ${styles.inactive('...........')} show the version number`
25 |
26 | module.exports = async (args, config) => {
27 | const defaultSubCmd = 'help'
28 | const subCmd = args._[0] === 'help'
29 | ? args._[1] || defaultSubCmd
30 | : args._[0] || defaultSubCmd
31 |
32 | try {
33 | const module = require(`./${subCmd}`)
34 | console.log(module.menu || mainMenu)
35 | } catch (err) {
36 | if (err.code === 'MODULE_NOT_FOUND') {
37 | throw new Error(`"${subCmd}" does not have a help menu!`)
38 | }
39 | throw err
40 | }
41 | }
42 |
--------------------------------------------------------------------------------
/src/cmds/init.js:
--------------------------------------------------------------------------------
1 | const syspath = require('path')
2 | const fs = require('fs-extra')
3 | const { copyDir } = require('../core/filesystem')
4 | const { ask, confirm } = require('../utils/readline')
5 | const { createConfig } = require('../utils/config')
6 | const { styles, log } = require('../utils/emit')
7 |
8 | const STARTER_DIR = syspath.resolve(__dirname, '../../starter')
9 |
10 | module.exports = async (args, config) => {
11 | const name = await ask('What is the name of your project?', {
12 | default: syspath.basename(process.cwd()),
13 | })
14 |
15 | const existingDocs = args._[1] ||
16 | await confirm('Do you already have some docs?', { default: 'y' })
17 |
18 | const root = args._[1] || (existingDocs
19 | ? await ask('Where are your doc files?', { default: config.root })
20 | : 'docs/')
21 |
22 | const file = await createConfig(name, root)
23 | log(`Created new config file at ${styles.note(file)}`, true)
24 |
25 | if (await fs.pathExists(root)) {
26 | log(`Using ${styles.note(root)} as your docs folder`)
27 | } else {
28 | log(`Creating some documentation at ${styles.note(root)}`)
29 | await copyDir(STARTER_DIR, root)
30 | }
31 |
32 | log(`Ready to go! Run ${styles.note('gitdocs serve')} to get started`)
33 | }
34 |
35 | module.exports.menu = `
36 | ${styles.title('Usage')}
37 |
38 | gitdocs init [dir] [options]
39 |
40 | ${styles.title('Options')}
41 |
42 | --name, -n ${styles.inactive('..............')} specify a name for your project`
43 |
--------------------------------------------------------------------------------
/src/cmds/manifest.js:
--------------------------------------------------------------------------------
1 | const { dirTree } = require('../core/filesystem')
2 | const { hydrateTree } = require('../core/hydrate')
3 |
4 | module.exports = async (args, config) => {
5 | const tree = await dirTree(config.root)
6 | const { manifest } = await hydrateTree(tree, config)
7 |
8 | const manifestStr = JSON.stringify(manifest, null, 2)
9 |
10 | console.log(manifestStr)
11 | }
12 |
--------------------------------------------------------------------------------
/src/cmds/serve.js:
--------------------------------------------------------------------------------
1 | const core = require('../core')
2 | const startServer = require('../core/server')
3 | const { styles, log, fullScreen } = require('../utils/emit')
4 |
5 | module.exports = async (args, config) => {
6 | fullScreen()
7 | log('Starting local development server', true)
8 |
9 | const {
10 | props,
11 | compiler,
12 | } = await core('development', config)
13 |
14 | const server = await startServer(props, compiler)
15 | log(`[\u2713] Docs are live at ${styles.url(server.url)}`)
16 | }
17 |
18 | module.exports.menu = `
19 | ${styles.title('Usage')}
20 |
21 | gitdocs serve [options]
22 | ${styles.subnote('or gitdocs start')}
23 |
24 | ${styles.title('Options')}
25 |
26 | ${styles.subnote('no options yet')}
27 | `
28 |
--------------------------------------------------------------------------------
/src/cmds/version.js:
--------------------------------------------------------------------------------
1 | const { version } = require('../../package.json')
2 | const { log } = require('../utils/emit')
3 |
4 | module.exports = async (args, config) => {
5 | log(`GitDocs v${version}`, true)
6 | }
7 |
--------------------------------------------------------------------------------
/src/core/compiler.js:
--------------------------------------------------------------------------------
1 | const syspath = require('path')
2 | const webpack = require('webpack')
3 | const ProgressPlugin = require('webpack/lib/ProgressPlugin')
4 | const { babelOptions } = require('../utils/babel')
5 |
6 | const THEMES_DIR = syspath.resolve(__dirname, '../../themes')
7 | const NODE_MODS_DIR = syspath.resolve(__dirname, '../../node_modules')
8 |
9 | async function getCompiler (env, props) {
10 | const isDev = env === 'development'
11 |
12 | return webpack({
13 | mode: env,
14 | context: syspath.resolve(__dirname, '../../'),
15 | devtool: isDev
16 | ? 'cheap-module-eval-source-map'
17 | : 'cheap-module-source-map',
18 | entry: {
19 | main: [
20 | isDev && 'webpack-hot-middleware/client',
21 | `${THEMES_DIR}/${props.config.theme}/index.js`,
22 | ].filter(Boolean),
23 | },
24 | output: {
25 | filename: '[name].[hash].js',
26 | chunkFilename: '[chunkhash].chunk.js',
27 | path: syspath.resolve(props.config.output),
28 | publicPath: '/',
29 | },
30 | resolve: {
31 | modules: [
32 | props.config.temp,
33 | NODE_MODS_DIR,
34 | 'node_modules',
35 | ],
36 | },
37 | module: {
38 | rules: [
39 | {
40 | test: /\.js$/,
41 | include: THEMES_DIR,
42 | exclude: NODE_MODS_DIR,
43 | use: {
44 | loader: 'babel-loader',
45 | options: babelOptions(),
46 | },
47 | },
48 | {
49 | test: /\.(png)$/,
50 | use: 'file-loader',
51 | },
52 | ],
53 | },
54 | plugins: [
55 | isDev &&
56 | new webpack.HotModuleReplacementPlugin(),
57 |
58 | new webpack.DefinePlugin({
59 | 'process.env': JSON.stringify({
60 | NODE_ENV: env,
61 | PROPS: props,
62 | }),
63 | }),
64 | ].filter(Boolean),
65 | })
66 | }
67 |
68 | function getCompilerProgress (compiler, cb) {
69 | let progressLimit = 0
70 |
71 | new ProgressPlugin((p, msg) => {
72 | const percentage = Math.floor(p * 100)
73 |
74 | if (percentage > progressLimit) {
75 | cb(percentage - progressLimit, msg)
76 | progressLimit = percentage
77 | }
78 | }).apply(compiler)
79 | }
80 |
81 | function buildBundle (compiler) {
82 | return new Promise((resolve, reject) => {
83 | compiler.run((err, stats) => {
84 | const info = stats.toJson()
85 |
86 | err || stats.hasErrors()
87 | ? reject(info.errors.length ? info.errors : err)
88 | : resolve(info)
89 | })
90 | })
91 | }
92 |
93 | module.exports = {
94 | getCompiler,
95 | getCompilerProgress,
96 | buildBundle,
97 | }
98 |
--------------------------------------------------------------------------------
/src/core/compiler.spec.js:
--------------------------------------------------------------------------------
1 | describe('unit: core/compiler', () => {
2 |
3 | })
4 |
--------------------------------------------------------------------------------
/src/core/components.js:
--------------------------------------------------------------------------------
1 | // import path from 'path'
2 | // import React from 'react'
3 | // import { megaGlob } from '../utils/filesystem'
4 | // import './register'
5 | //
6 | // const COMPONENTS_DIR = '_components'
7 | //
8 | // export default async function (baseDir) {
9 | // const componentMap = {}
10 | //
11 | // const dir = path.resolve(baseDir, COMPONENTS_DIR)
12 | // const files = await megaGlob([
13 | // `${dir}/*.js`,
14 | // `${dir}/*/index.js`,
15 | // ], {
16 | // nodir: true,
17 | // })
18 | //
19 | // // so components don't have to require react
20 | // global.React = React
21 | //
22 | // files.forEach(file => {
23 | // const name = /index\.js$/.test(file)
24 | // ? path.basename(path.resolve(file, '..'))
25 | // : path.basename(file, '.js')
26 | //
27 | // const module = require(file)
28 | // componentMap[name] = {
29 | // component: module.default || module,
30 | // props: {
31 | // // any extra props for custom components?
32 | // },
33 | // }
34 | // })
35 | //
36 | // return componentMap
37 | // }
38 |
--------------------------------------------------------------------------------
/src/core/components.spec.js:
--------------------------------------------------------------------------------
1 | describe('unit: core/components', () => {
2 |
3 | })
4 |
--------------------------------------------------------------------------------
/src/core/database.js:
--------------------------------------------------------------------------------
1 | const { getContent } = require('./filesystem')
2 |
3 | async function generateDatabase (manifest) {
4 | const db = []
5 |
6 | const _recursive = async ({ items, ...item }) => {
7 | if (item.input) {
8 | db.push({
9 | url: item.url,
10 | title: item.title,
11 | tags: item.tags,
12 | related: item.related,
13 | breadcrumbs: item.breadcrumbs,
14 | content: await getContent(item.input),
15 | })
16 | }
17 |
18 | if (items) {
19 | await Promise.all(
20 | items.map(i => _recursive(i))
21 | )
22 | }
23 | }
24 |
25 | await _recursive(manifest)
26 | return db
27 | }
28 |
29 | module.exports = {
30 | generateDatabase,
31 | }
32 |
--------------------------------------------------------------------------------
/src/core/database.spec.js:
--------------------------------------------------------------------------------
1 | describe('unit: core/database', () => {
2 |
3 | })
4 |
--------------------------------------------------------------------------------
/src/core/filesystem.js:
--------------------------------------------------------------------------------
1 | const fs = require('fs-extra')
2 | const syspath = require('path')
3 | const { ncp } = require('ncp')
4 | const { parseFrontmatter } = require('../utils/frontmatter')
5 |
6 | const INDEX_FILES = ['index', 'readme']
7 |
8 | /**
9 | * whether a pathname matches an index file.
10 | */
11 | function isIndexFile (path, returnIndex) {
12 | const filename = syspath
13 | .basename(path, syspath.extname(path))
14 | .toLowerCase()
15 |
16 | const idx = INDEX_FILES
17 | .findIndex(file => file === filename)
18 |
19 | return returnIndex
20 | ? idx
21 | : idx > -1
22 | }
23 |
24 | /**
25 | * checks for conflicting filenames on a path,
26 | * such as a duplicated index file or a folder
27 | * with the same base name.
28 | */
29 | async function checkForConflicts (path) {
30 | const fileIdx = isIndexFile(path, true)
31 | const dir = syspath.dirname(path)
32 |
33 | const ext = syspath.extname(path)
34 | const basePath = syspath.basename(path, ext)
35 | const pathAsDir = syspath.join(dir, basePath)
36 |
37 | // don't allow something/ and something.md
38 | if (path !== pathAsDir && await fs.pathExists(pathAsDir)) {
39 | throw new Error(`Conflicting paths found (use an index file instead):\n\t- ${pathAsDir}/\n\t- ${path}`)
40 | }
41 |
42 | // don't allow index.md and readme.md
43 | fileIdx > -1 && await Promise.all(
44 | INDEX_FILES.map(async (file, idx) => {
45 | const checkPath = syspath.join(dir, `${file}${ext}`)
46 |
47 | // should not conflict with itself
48 | if (idx === fileIdx) {
49 | return
50 | }
51 |
52 | // another index file exists
53 | if (await fs.pathExists(checkPath)) {
54 | throw new Error(`Conflicting index files were found:\n\t- ${checkPath}\n\t- ${path}`)
55 | }
56 | })
57 | )
58 | }
59 |
60 | /**
61 | * walk a directory and return data for each item,
62 | * preserving the tree structure.
63 | */
64 | async function dirTree (baseDir, opts = {}) {
65 | if (!await fs.pathExists(baseDir)) {
66 | throw new Error(`Could not find root folder: ${baseDir}`)
67 | }
68 |
69 | // only include files with these extensions
70 | opts.extensions = opts.extensions || ['.md']
71 |
72 | // ignore everything prefixed with dot by default
73 | opts.exclude = opts.exclude || /^\../
74 |
75 | if (!Array.isArray(opts.extensions)) {
76 | throw new TypeError('Extensions must be an array!')
77 | }
78 |
79 | if (!(opts.exclude instanceof RegExp)) {
80 | throw new TypeError('Excluded pattern must be a regular expression!')
81 | }
82 |
83 | const _walk = async path => {
84 | const item = {
85 | // resolve the path to an absolute path
86 | path: syspath.resolve(path),
87 | // keep the relative path so we can easily construct a url
88 | // NOTE: needs to be an empty string for the base dir, so it doesn't become part of the url
89 | path_relative: syspath.basename(path === baseDir ? '' : path),
90 | }
91 |
92 | // analyze the path, file or folder
93 | const stats = await fs.stat(item.path)
94 | const ext = syspath.extname(item.path)
95 |
96 | // make sure to exclude any paths that match the pattern
97 | if (opts.exclude.test(item.path_relative)) {
98 | return
99 | }
100 |
101 | // if a folder, recurse into it and run this function again
102 | if (stats.isDirectory()) {
103 | item.type = 'directory'
104 |
105 | // recursivily walk all sub paths and
106 | const childrenItems = await fs.readdir(item.path)
107 | const childrenUnfiltered = await Promise.all(
108 | childrenItems.map(f => _walk(syspath.join(item.path, f)))
109 | )
110 |
111 | // filter out all items that were excluded
112 | const children = childrenUnfiltered.filter(Boolean)
113 |
114 | // find the idx of the children's main index file
115 | const index = children.findIndex(f => isIndexFile(f.path))
116 |
117 | // whether the directory has an index file
118 | if (index > -1) {
119 | item.childrenIndex = index
120 | }
121 |
122 | // get rid of items that were excluded
123 | item.children = children
124 | }
125 |
126 | // if a file, can't recurse any further
127 | if (stats.isFile()) {
128 | item.type = 'file'
129 |
130 | // only include whitelisted filetypes
131 | if (opts.extensions.indexOf(ext) === -1) {
132 | return
133 | }
134 |
135 | // ensure there are no file conflicts like duplicate indexes
136 | await checkForConflicts(item.path)
137 |
138 | // index file path is put on it's directory level (the parent)
139 | if (isIndexFile(item.path)) {
140 | item.index = true
141 | }
142 | }
143 |
144 | return item
145 | }
146 |
147 | // kick off recursive function with base directory
148 | return _walk(baseDir)
149 | }
150 |
151 | async function getContent (path) {
152 | const fileContent = await fs.readFile(path)
153 | const { content } = parseFrontmatter(fileContent)
154 |
155 | return content
156 | }
157 |
158 | /**
159 | * because of https://github.com/zeit/pkg/issues/420
160 | */
161 | function copyDir (from, to) {
162 | return new Promise((resolve, reject) => {
163 | ncp(from, to, (err) => {
164 | if (err) reject(err)
165 | else resolve()
166 | })
167 | })
168 | }
169 |
170 | module.exports = {
171 | isIndexFile,
172 | checkForConflicts,
173 | dirTree,
174 | getContent,
175 | copyDir,
176 | }
177 |
178 | module.exports.indexFilenames = INDEX_FILES
179 |
--------------------------------------------------------------------------------
/src/core/filesystem.spec.js:
--------------------------------------------------------------------------------
1 | const { expect } = require('code')
2 | const mockFs = require('mock-fs')
3 | const filesystem = require('./filesystem')
4 |
5 | describe('unit: utils/filesystem', () => {
6 | afterEach(mockFs.restore)
7 | beforeEach(() => mockFs({
8 | '/mock': {
9 | '.foo': 'some hidden file',
10 | 'foo.json': 'some config file',
11 | 'foo.md': '---\ntitle: FOO\n---\ncontent of file 0',
12 | 'index.md': 'content of index file',
13 | bar: {
14 | 'readme.md': 'content of index',
15 | 'file1.md': 'content of file 1',
16 | baz: {
17 | qux: {
18 | 'file3.md': 'content of file 3',
19 | 'file4.md': 'content of file 4',
20 | 'file5.json': 'some config file',
21 | },
22 | },
23 | '.baz': {
24 | 'qux.md': 'some hidden folder',
25 | },
26 | },
27 | },
28 | }))
29 |
30 | it('indexFilenames', async () => {
31 | expect(filesystem.indexFilenames).to.have.length(2)
32 | expect(filesystem.indexFilenames[0]).to.equal('index')
33 | })
34 |
35 | it('isIndexFile()', async () => {
36 | expect(filesystem.isIndexFile('readme')).to.be.true()
37 | expect(filesystem.isIndexFile('readme.md')).to.be.true()
38 | expect(filesystem.isIndexFile('readme.md', true)).to.equal(1)
39 | expect(filesystem.isIndexFile('README.MD')).to.be.true()
40 | expect(filesystem.isIndexFile('Readme.md')).to.be.true()
41 | expect(filesystem.isIndexFile('index')).to.be.true()
42 | expect(filesystem.isIndexFile('index.md')).to.be.true()
43 | expect(filesystem.isIndexFile('index.md', true)).to.equal(0)
44 | expect(filesystem.isIndexFile('INDEX.md')).to.be.true()
45 | expect(filesystem.isIndexFile('foo/bar/readme.md')).to.be.true()
46 | expect(filesystem.isIndexFile('foo/bar/index.md')).to.be.true()
47 | expect(filesystem.isIndexFile('foo/bar/readme')).to.be.true()
48 | expect(filesystem.isIndexFile('foo/bar/index')).to.be.true()
49 | expect(filesystem.isIndexFile('foobar.md')).to.be.false()
50 | })
51 |
52 | describe('checkForConflicts()', async () => {
53 | it('no conflicts', async () => {
54 | mockFs({ 'index.md': '' })
55 | await expect(filesystem.checkForConflicts('index.md')).to.not.reject()
56 | })
57 |
58 | it('conflicting index files', async () => {
59 | mockFs({ 'index.md': '', 'readme.md': '' })
60 | await expect(filesystem.checkForConflicts('index.md')).to.reject(Error)
61 | })
62 |
63 | it('conflicting file and folder', async () => {
64 | mockFs({ 'foo.md': '', foo: { 'index.md': '' } })
65 | await expect(filesystem.checkForConflicts('foo.md')).to.reject(Error)
66 | })
67 | })
68 |
69 | describe('dirTree()', () => {
70 | it('normal', async () => {
71 | const res = await filesystem.dirTree('/mock')
72 | expect(res).to.equal({
73 | path: '/mock',
74 | path_relative: '',
75 | type: 'directory',
76 | childrenIndex: 2,
77 | children: [
78 | {
79 | path: '/mock/bar',
80 | path_relative: 'bar',
81 | type: 'directory',
82 | childrenIndex: 2,
83 | children: [
84 | {
85 | path: '/mock/bar/baz',
86 | path_relative: 'baz',
87 | type: 'directory',
88 | children: [
89 | {
90 | path: '/mock/bar/baz/qux',
91 | path_relative: 'qux',
92 | type: 'directory',
93 | children: [
94 | {
95 | path: '/mock/bar/baz/qux/file3.md',
96 | path_relative: 'file3.md',
97 | type: 'file',
98 | },
99 | {
100 | path: '/mock/bar/baz/qux/file4.md',
101 | path_relative: 'file4.md',
102 | type: 'file',
103 | },
104 | ],
105 | },
106 | ],
107 | },
108 | {
109 | path: '/mock/bar/file1.md',
110 | path_relative: 'file1.md',
111 | type: 'file',
112 | },
113 | {
114 | path: '/mock/bar/readme.md',
115 | path_relative: 'readme.md',
116 | type: 'file',
117 | index: true,
118 | },
119 | ],
120 | },
121 | {
122 | path: '/mock/foo.md',
123 | path_relative: 'foo.md',
124 | type: 'file',
125 | },
126 | {
127 | path: '/mock/index.md',
128 | path_relative: 'index.md',
129 | type: 'file',
130 | index: true,
131 | },
132 | ]
133 | })
134 | })
135 |
136 | it('duplicated index', async () => {
137 | mockFs({ foo: { 'index.md': '', 'readme.md': '' } })
138 | await expect(filesystem.dirTree('foo')).to.reject(Error)
139 | })
140 |
141 | it('invalid options', async () => {
142 | await expect(filesystem.dirTree('/', {
143 | exclude: '**',
144 | })).to.reject(TypeError)
145 | await expect(filesystem.dirTree('/', {
146 | extensions: '.md',
147 | })).to.reject(TypeError)
148 | })
149 |
150 | it('non-existant folder', async () => {
151 | await expect(filesystem.dirTree('whatup')).to.reject(Error, 'Could not find root folder: whatup')
152 | })
153 | })
154 |
155 | it('getContent()', async () => {
156 | const content = await filesystem.getContent('/mock/foo.md')
157 | expect(content).to.equal('content of file 0')
158 | })
159 |
160 | it('copyDir()')
161 | })
162 |
--------------------------------------------------------------------------------
/src/core/hydrate.js:
--------------------------------------------------------------------------------
1 | const syspath = require('path')
2 | const markdownToc = require('markdown-toc')
3 | const ourpath = require('../utils/path')
4 | const { getFrontmatterOnly } = require('../utils/frontmatter')
5 | const { mergeLeftByKey } = require('../utils/merge')
6 | const { getContent } = require('./filesystem')
7 | const { walkSource } = require('./source')
8 | const Sitemap = require('./sitemap')
9 |
10 | async function getMetaData (item, parentItems) {
11 | const data = item.type === 'file'
12 | ? await getFrontmatterOnly(item.path)
13 | : {}
14 |
15 | // @TODO warn about conflicting items in parent and self
16 | // inherit front matter from the parent sidebar config
17 | const dataFromParent = parentItems
18 | .find(i => i.path === item.path_relative)
19 |
20 | return { ...data, ...dataFromParent }
21 | }
22 |
23 | function normalizeItems (data) {
24 | const itemsMessy =
25 | data.items ||
26 | data.items_prepend ||
27 | data.items_append ||
28 | []
29 |
30 | const items = itemsMessy.map(item => {
31 | if (typeof item !== 'string' && typeof item !== 'object') {
32 | throw new TypeError(`Each item must be a string or object, got ${typeof item}`)
33 | }
34 |
35 | return typeof item === 'string'
36 | ? { path: item }
37 | : item
38 | })
39 |
40 | const hasProp = Object.prototype.hasOwnProperty
41 | const strategy = hasProp.call(data, 'items')
42 | ? 'replace' : hasProp.call(data, 'items_prepend')
43 | ? 'prepend' : hasProp.call(data, 'items_append')
44 | ? 'append' : null
45 |
46 | return {
47 | items,
48 | strategy,
49 | }
50 | }
51 |
52 | async function tableOfContents (toc, { input, items }) {
53 | // only add items that have a file associated with it
54 | if (input) {
55 | if (toc.page) {
56 | toc.page = markdownToc(await getContent(input))
57 | .json.filter(i => i.lvl <= 2)
58 | }
59 |
60 | if (toc.folder) {
61 | toc.folder = items
62 | // only want children items that have an input
63 | .filter(item => item.input)
64 | // reduced data, since we don't need everything
65 | .map(item => ({
66 | title: item.title,
67 | description: item.description,
68 | url: item.url,
69 | }))
70 | }
71 | }
72 |
73 | // dont keep empty arrays
74 | if (!toc.page || !toc.page.length) delete toc.page
75 | if (!toc.folder || !toc.folder.length) delete toc.folder
76 |
77 | return toc
78 | }
79 |
80 | async function hydrateTree (tree, config, opts = {}) {
81 | const sitemap = new Sitemap()
82 |
83 | if (tree.childrenIndex === undefined) {
84 | throw new Error('No index file was found! Create a `readme.md` at the root of your project.')
85 | }
86 |
87 | const _recursive = async (
88 | item,
89 | itemParent = {},
90 | itemParentItems = [],
91 | ) => {
92 | const {
93 | path_relative,
94 | childrenIndex,
95 | children = [],
96 | } = item
97 |
98 | // hoist the index file and use it instead of the current item,
99 | // if there is an index file under it's children
100 | const hoistedItem = childrenIndex !== undefined
101 | ? item.children[childrenIndex]
102 | : item
103 |
104 | // extract front matter from file while inheriting data from parent
105 | const metaData = await getMetaData(hoistedItem, itemParentItems)
106 |
107 | // start hydrating the current item
108 | const hydratedItem = {
109 | path: path_relative,
110 | title: metaData.title || (
111 | itemParent.path !== undefined
112 | // convert the file path into the title
113 | ? ourpath.titlify(hoistedItem.path)
114 | // use the project name as the title if we are at the root
115 | : config.name
116 | ),
117 | url: ourpath.routify(
118 | syspath.resolve(
119 | '/', // don't resolve from the cwd
120 | itemParent.url || itemParent.path || config.baseURL,
121 | metaData.url || path_relative,
122 | )
123 | ),
124 | }
125 |
126 | if (metaData.draft && !opts.includeDrafts) {
127 | return
128 | }
129 |
130 | // add these items from metadata, but only if not undefined
131 | if (metaData.hidden) hydratedItem.hidden = true
132 | if (metaData.description) hydratedItem.description = metaData.description
133 | if (metaData.tags) hydratedItem.tags = metaData.tags.split(',').map(i => i.trim())
134 | if (metaData.related) {
135 | hydratedItem.related = metaData.related
136 | .map(url => ({ title: ourpath.titlify(url), url }))
137 | }
138 |
139 | // continue the breadcrumb from parent
140 | if (config.breadcrumbs && metaData.breadcrumbs !== false) {
141 | const breadcrumb = { title: hydratedItem.title }
142 |
143 | const breadcrumbs = []
144 | const breadcrumbsParent = itemParent.breadcrumbs || []
145 |
146 | if (hoistedItem.type === 'file') {
147 | breadcrumb.url = hydratedItem.url
148 | }
149 |
150 | breadcrumbsParent
151 | .concat(breadcrumb)
152 | // only add unique urls to the breadcrumb
153 | .forEach(crumb =>
154 | breadcrumbs.findIndex(i => i.url === crumb.url) === -1 &&
155 | breadcrumbs.push(crumb)
156 | )
157 |
158 | hydratedItem.breadcrumbs = breadcrumbs
159 | }
160 |
161 | // only files should have an input and output value
162 | if (hoistedItem.type === 'file') {
163 | hydratedItem.input = hoistedItem.path
164 | hydratedItem.outputDir = syspath.join(config.output, hydratedItem.url)
165 |
166 | // pull in source items if one exists
167 | if (metaData.source) {
168 | const source = await walkSource(config.temp, hoistedItem.path, metaData)
169 | const sourceHydrated = await _recursive(source, hydratedItem)
170 |
171 | // don't inherit these items from the source
172 | delete sourceHydrated.path
173 | delete sourceHydrated.title
174 |
175 | // replace current item data with the source data
176 | Object.assign(hydratedItem, sourceHydrated)
177 | // don't register the url when there is a source (since item gets replaced)
178 | } else {
179 | // add url to the sitemap
180 | sitemap.addUrl(`${config.domain}${hydratedItem.url}`, {
181 | ...metaData.sitemap,
182 | filename: hoistedItem.path,
183 | })
184 | }
185 | }
186 |
187 | // get sub items from the front matter
188 | const {
189 | items: metaDataItems,
190 | strategy: mergeStrategy,
191 | } = normalizeItems(metaData)
192 |
193 | // recurse sub items from the dir tree
194 | const childrenItemsUnsorted = await Promise.all(
195 | children
196 | .filter(({ index }) => !index)
197 | .map(childItem => _recursive(childItem, hydratedItem, metaDataItems))
198 | )
199 |
200 | // sort alphabetically by default
201 | const childrenSorted = childrenItemsUnsorted
202 | .filter(Boolean)
203 | .sort((a, b) => a.title.localeCompare(b.title))
204 |
205 | // @TODO: figure out how to remove items_pre/append from the result
206 | const mergedItems = mergeLeftByKey(metaDataItems, childrenSorted, {
207 | key: 'path',
208 | name: hoistedItem.path,
209 | strategy: mergeStrategy,
210 | })
211 |
212 | hydratedItem.items = [
213 | ...mergedItems || [],
214 | ...hydratedItem.items || [],
215 | ]
216 |
217 | // don't keep an empty items array
218 | if (!hydratedItem.items.length) {
219 | delete hydratedItem.items.length
220 | }
221 |
222 | // add table of contents, if applicable
223 | hydratedItem.toc = await tableOfContents(
224 | Object.assign({}, config.table_of_contents, metaData.table_of_contents),
225 | hydratedItem,
226 | )
227 |
228 | return hydratedItem
229 | }
230 |
231 | return {
232 | manifest: await _recursive(tree),
233 | sitemap: sitemap.generate(),
234 | }
235 | }
236 |
237 | // async function hydrateContent (manifest) {
238 | // const _recursive = async (item) => {
239 | // if (item.input) {
240 | // item.content = await getContent(item.input)
241 | // }
242 | //
243 | // if (item.items) {
244 | // item.items = await Promise.all(
245 | // item.items.map(i => _recursive(i))
246 | // )
247 | // }
248 | //
249 | // return item
250 | // }
251 | //
252 | // return _recursive(manifest)
253 | // }
254 |
255 | module.exports = {
256 | getMetaData,
257 | normalizeItems,
258 | tableOfContents,
259 | hydrateTree,
260 | // hydrateContent,
261 | }
262 |
--------------------------------------------------------------------------------
/src/core/hydrate.spec.js:
--------------------------------------------------------------------------------
1 | describe('unit: core/hydrate', () => {
2 |
3 | })
4 |
--------------------------------------------------------------------------------
/src/core/index.js:
--------------------------------------------------------------------------------
1 | const loadSyntax = require('./syntax')
2 | const staticAssets = require('./static')
3 | const { dirTree } = require('./filesystem')
4 | const { hydrateTree } = require('./hydrate')
5 | const { getRobotsTxt } = require('./robots')
6 | const { getCompiler } = require('./compiler')
7 | const { log } = require('../utils/emit')
8 |
9 | module.exports = async (env, localConfig) => {
10 | // Load only supported syntaxes to reduce bundle size
11 | await loadSyntax(localConfig)
12 |
13 | // Load static assets like images, scripts, css, etc.
14 | await staticAssets(localConfig, env === 'development')
15 |
16 | // generate and hydrate the manifest
17 | const tree = await dirTree(localConfig.root)
18 | const hydrated = await hydrateTree(tree, localConfig, {
19 | includeDrafts: env === 'development',
20 | })
21 |
22 | log('Generated and hydrated manifest')
23 |
24 | // this gets passed to the theme
25 | const props = {
26 | config: localConfig,
27 | manifest: hydrated.manifest,
28 | sitemap: hydrated.sitemap,
29 | robots: getRobotsTxt(localConfig),
30 | }
31 |
32 | // setup webpack compiler so we can build (or watch)
33 | const compiler = await getCompiler(env, props)
34 |
35 | return {
36 | props,
37 | compiler,
38 | }
39 | }
40 |
--------------------------------------------------------------------------------
/src/core/index.spec.js:
--------------------------------------------------------------------------------
1 | describe('unit: core/index', () => {
2 |
3 | })
4 |
--------------------------------------------------------------------------------
/src/core/output.js:
--------------------------------------------------------------------------------
1 | const fs = require('fs-extra')
2 | const syspath = require('path')
3 | const { warn } = require('../utils/emit')
4 | const { generateDatabase } = require('./database')
5 | const { templateForProduction } = require('./template')
6 | const { getContent } = require('./filesystem')
7 |
8 | module.exports = async (entrypoints, props) => {
9 | const outputDB = syspath.join(props.config.output, 'db.json')
10 | const outputSitemap = syspath.join(props.config.output, 'sitemap.xml')
11 | const outputRobots = syspath.join(props.config.output, 'robots.txt')
12 |
13 | await fs.outputJson(outputDB, await generateDatabase(props.manifest))
14 | await fs.outputFile(outputSitemap, props.sitemap)
15 |
16 | await fs.pathExists(outputRobots)
17 | ? warn('You have a custom robots.txt file, so one was not generated for you!')
18 | : await fs.outputFile(outputRobots, props.robots)
19 |
20 | const _recursive = async ({ items, ...item }) => {
21 | if (item.outputDir) {
22 | item.content = await getContent(item.input)
23 | const template = await templateForProduction(entrypoints, props, item)
24 |
25 | const outputHtml = syspath.join(item.outputDir, 'index.html')
26 | const outputJson = syspath.join(item.outputDir, 'index.json')
27 |
28 | await fs.outputFile(outputHtml, template)
29 | await fs.outputJson(outputJson, {
30 | title: item.title,
31 | content: item.content,
32 | })
33 | }
34 |
35 | if (items) {
36 | await Promise.all(
37 | items.map(i => _recursive(i))
38 | )
39 | }
40 | }
41 |
42 | return _recursive(props.manifest)
43 | }
44 |
--------------------------------------------------------------------------------
/src/core/output.spec.js:
--------------------------------------------------------------------------------
1 | describe('unit: core/output', () => {
2 |
3 | })
4 |
--------------------------------------------------------------------------------
/src/core/robots.js:
--------------------------------------------------------------------------------
1 | function getRobotsTxt (config = {}) {
2 | const lines = [
3 | 'User-agent: *',
4 | `Sitemap: ${config.domain || ''}/sitemap.xml`,
5 | `Disallow: ${config.crawlable ? '' : '/'}`,
6 | ]
7 |
8 | return lines.join('\n')
9 | }
10 |
11 | module.exports = {
12 | getRobotsTxt,
13 | }
14 |
--------------------------------------------------------------------------------
/src/core/robots.spec.js:
--------------------------------------------------------------------------------
1 | const { expect } = require('code')
2 | const robots = require('./robots')
3 |
4 | describe('unit: core/robots', () => {
5 | it('getRobotsTxt()', async () => {
6 | expect(robots.getRobotsTxt()).to.equal('User-agent: *\nSitemap: /sitemap.xml\nDisallow: /')
7 | expect(robots.getRobotsTxt({
8 | domain: 'https://foo.bar',
9 | crawlable: true,
10 | })).to.equal('User-agent: *\nSitemap: https://foo.bar/sitemap.xml\nDisallow: ')
11 | })
12 | })
13 |
--------------------------------------------------------------------------------
/src/core/server.js:
--------------------------------------------------------------------------------
1 | const http = require('http')
2 | const connect = require('connect')
3 | const serveStatic = require('serve-static')
4 | const webpackDevMiddleware = require('webpack-dev-middleware')
5 | const webpackHotMiddleware = require('webpack-hot-middleware')
6 | const { namespaces } = require('../utils/temp')
7 | const { generateDatabase } = require('./database')
8 | const { templateForDevelopment } = require('./template')
9 | const attachSocket = require('./socket')
10 |
11 | module.exports = (props, compiler) => {
12 | const {
13 | host,
14 | port,
15 | temp,
16 | } = props.config
17 |
18 | const app = connect()
19 | const url = `http://${host}:${port}`
20 | const staticDir = `${temp}/${namespaces.static}`
21 |
22 | const compilerInstance = webpackDevMiddleware(compiler, {
23 | logLevel: 'warn',
24 | serverSideRender: true,
25 | })
26 |
27 | const compilerHotInstance = webpackHotMiddleware(compiler, {
28 | log: false,
29 | })
30 |
31 | app.use(compilerInstance)
32 | app.use(compilerHotInstance)
33 |
34 | app.use(serveStatic(staticDir, {
35 | index: false,
36 | }))
37 |
38 | app.use('/robots.txt', (req, res, next) => {
39 | res.setHeader('Content-Type', 'text/plain; charset=utf-8')
40 | res.end(props.robots)
41 | })
42 |
43 | app.use('/sitemap.xml', (req, res, next) => {
44 | res.setHeader('Content-Type', 'application/xml; charset=utf-8')
45 | res.end(props.sitemap)
46 | })
47 |
48 | app.use('/db.json', async (req, res, next) => {
49 | try {
50 | const db = await generateDatabase(props.manifest)
51 |
52 | res.setHeader('Content-Type', 'application/json; charset=utf-8')
53 | res.end(JSON.stringify(db))
54 | } catch (err) {
55 | next(err)
56 | }
57 | })
58 |
59 | app.use(async (req, res, next) => {
60 | try {
61 | const { entrypoints } = res.locals.webpackStats.toJson()
62 | const rendered = await templateForDevelopment(entrypoints)
63 |
64 | res.setHeader('Content-Type', 'text/html; charset=utf-8')
65 | res.end(rendered)
66 | } catch (err) {
67 | next(err)
68 | }
69 | })
70 |
71 | app.use((err, req, res, next) => {
72 | console.error(err)
73 | res.end('an error occurred!')
74 |
75 | next()
76 | })
77 |
78 | const server = http.createServer(app)
79 | attachSocket(server)
80 |
81 | return new Promise((resolve, reject) => {
82 | server.listen(port, host, err => {
83 | if (err) {
84 | return reject(err)
85 | }
86 |
87 | compilerInstance.waitUntilValid(() => {
88 | resolve({ url })
89 | })
90 | })
91 | })
92 | }
93 |
--------------------------------------------------------------------------------
/src/core/server.spec.js:
--------------------------------------------------------------------------------
1 | describe('unit: core/server', () => {
2 |
3 | })
4 |
--------------------------------------------------------------------------------
/src/core/sitemap.js:
--------------------------------------------------------------------------------
1 | const { minify } = require('html-minifier')
2 |
3 | class Sitemap {
4 | constructor () {
5 | this.urls = []
6 | this.urlMap = {}
7 | }
8 |
9 | addUrl (url, data = {}) {
10 | const filename = data.filename || 'unknown_file'
11 | const merged = Object.assign({}, {
12 | loc: url,
13 | priority: 0.5,
14 | changefreq: null,
15 | lastmod: null,
16 | }, data)
17 |
18 | if (this.urlMap[url]) {
19 | const duplicated = [url, filename, this.urlMap[url]]
20 | throw new Error(`Duplicated URL was found: ${duplicated.join('\n\t- ')}`)
21 | }
22 |
23 | this.urlMap[url] = filename
24 | this.urls.push(merged)
25 | }
26 |
27 | generate () {
28 | return minify(`
29 |
30 |
31 | ${this.urls.map(data => `
32 |
33 | ${data.loc ? `${data.loc} ` : ''}
34 | ${data.lastmod ? `${data.lastmod} ` : ''}
35 | ${data.changefreq ? `${data.changefreq} ` : ''}
36 | ${data.priority ? `${data.priority} ` : ''}
37 |
38 | `).join('\n')}
39 |
40 | `, {
41 | collapseWhitespace: true,
42 | })
43 | }
44 | }
45 |
46 | module.exports = Sitemap
47 |
--------------------------------------------------------------------------------
/src/core/sitemap.spec.js:
--------------------------------------------------------------------------------
1 | const { expect } = require('code')
2 | const Sitemap = require('./sitemap')
3 |
4 | describe('unit: core/sitemap', () => {
5 | it('Sitemap()', async () => {
6 | const sitemap = new Sitemap()
7 | sitemap.addUrl('/foo')
8 | sitemap.addUrl('/bar', { priority: 1 })
9 | sitemap.addUrl('/baz', { changefreq: 'daily' })
10 | expect(sitemap.generate()).to.equal(' /foo 0.5 /bar 1 /baz daily 0.5 ')
11 | })
12 |
13 | it('Sitemap() duplicated url', async () => {
14 | const sitemap = new Sitemap()
15 | sitemap.addUrl('/foo')
16 | expect(() => sitemap.addUrl('/foo')).to.throw(Error)
17 | })
18 | })
19 |
--------------------------------------------------------------------------------
/src/core/socket.js:
--------------------------------------------------------------------------------
1 | const fs = require('fs')
2 | const WebSocket = require('ws')
3 | const { getContent } = require('./filesystem')
4 |
5 | module.exports = (server) => {
6 | const socket = new WebSocket.Server({
7 | server
8 | })
9 |
10 | socket.on('connection', client => {
11 | let watcher
12 |
13 | client.on('message', file => {
14 | const send = async () => {
15 | const content = await getContent(file)
16 | client.send(JSON.stringify({
17 | content,
18 | }))
19 | }
20 |
21 | send()
22 |
23 | if (!watcher) {
24 | watcher = fs.watch(file, fileEvt => {
25 | if (fileEvt === 'change') {
26 | send()
27 | }
28 | })
29 | }
30 | })
31 |
32 | client.on('close', () => {
33 | if (watcher) {
34 | watcher.close()
35 | }
36 | })
37 | })
38 |
39 | return socket
40 | }
41 |
42 | // socket.broadcast = data =>
43 | // socket.clients.forEach(client =>
44 | // client.readyState === WebSocket.OPEN &&
45 | // client.send(JSON.stringify(data)))
46 |
--------------------------------------------------------------------------------
/src/core/socket.spec.js:
--------------------------------------------------------------------------------
1 | describe('unit: core/socket', () => {
2 |
3 | })
4 |
--------------------------------------------------------------------------------
/src/core/source.js:
--------------------------------------------------------------------------------
1 | const sourceGit = require('../sources/git')
2 | const sourceLocal = require('../sources/local')
3 | const { dirTree } = require('./filesystem')
4 | // const { log, styles } = require('../utils/emit')
5 |
6 | async function walkSource (tempDir, currentDir, data) {
7 | const {
8 | source,
9 | source_type: type = 'local',
10 | source_root: root = 'docs',
11 | source_branch: branch = 'master',
12 | } = data
13 |
14 | // log(`Fetching ${type} source: ${styles.note(source)}`)
15 |
16 | let resultDir
17 | switch (type) {
18 | case 'git': {
19 | resultDir = await sourceGit(tempDir, source, branch, root)
20 | break
21 | }
22 |
23 | case 'http':
24 | case 'https':
25 | case 'static': {
26 | throw new Error('HTTP sources are not supported yet.')
27 | }
28 |
29 | case 'local':
30 | default: {
31 | resultDir = await sourceLocal(source, currentDir)
32 | break
33 | }
34 | }
35 |
36 | return dirTree(resultDir)
37 | }
38 |
39 | module.exports = {
40 | walkSource,
41 | }
42 |
--------------------------------------------------------------------------------
/src/core/source.spec.js:
--------------------------------------------------------------------------------
1 | describe('unit: core/source', () => {
2 |
3 | })
4 |
--------------------------------------------------------------------------------
/src/core/static.js:
--------------------------------------------------------------------------------
1 | const fs = require('fs-extra')
2 | const syspath = require('path')
3 | const chokidar = require('chokidar')
4 | const { namespaces } = require('../utils/temp')
5 | const { log } = require('../utils/emit')
6 |
7 | function _watch (cwd, tempDir) {
8 | const watcher = chokidar.watch('**/*', {
9 | cwd,
10 | ignoreInitial: true,
11 | })
12 |
13 | watcher.on('all', (evt, path) => {
14 | const inputPath = `${cwd}/${path}`
15 | const outputPath = `${tempDir}/${path}`
16 |
17 | switch (evt) {
18 | case 'add':
19 | case 'change':
20 | fs.copySync(inputPath, outputPath)
21 | break
22 |
23 | case 'unlink':
24 | default:
25 | fs.removeSync(outputPath)
26 | break
27 | }
28 | })
29 | }
30 |
31 | module.exports = async (config, useTempDir) => {
32 | if (!await fs.pathExists(config.static)) {
33 | return
34 | }
35 |
36 | const dir = syspath.join(config.temp, namespaces.static)
37 |
38 | if (useTempDir) {
39 | await fs.copy(config.static, dir)
40 | _watch(config.static, dir)
41 | } else {
42 | await fs.copy(config.static, config.output)
43 | }
44 |
45 | log('[\u2713] Static assets loaded')
46 |
47 | return dir
48 | }
49 |
--------------------------------------------------------------------------------
/src/core/static.spec.js:
--------------------------------------------------------------------------------
1 | describe('unit: core/static', () => {
2 |
3 | })
4 |
--------------------------------------------------------------------------------
/src/core/syntax.js:
--------------------------------------------------------------------------------
1 | const fs = require('fs-extra')
2 | const syspath = require('path')
3 | const { log } = require('../utils/emit')
4 | const { namespaces } = require('../utils/temp')
5 |
6 | module.exports = async (config) => {
7 | const {
8 | temp,
9 | languages,
10 | syntax,
11 | } = config
12 |
13 | const rsh = syspath.dirname(require.resolve('react-syntax-highlighter'))
14 | const langs = languages
15 | .filter(lang => fs.pathExistsSync(`${rsh}/languages/${syntax.renderer}/${lang}.js`))
16 | .map(lang => `{ name: '${lang}', func: require('${rsh}/languages/${syntax.renderer}/${lang}').default },`)
17 | .join('\n')
18 |
19 | const content = `module.exports = {
20 | theme: require('${rsh}/styles/${syntax.renderer}/${syntax.theme}').default,
21 | languages: [${langs}]
22 | }`
23 |
24 | const path = `${temp}/${namespaces.codegen}/loadSyntax.js`
25 | await fs.outputFile(path, content)
26 | log('[\u2713] Syntax loaded')
27 | return path
28 | }
29 |
--------------------------------------------------------------------------------
/src/core/syntax.spec.js:
--------------------------------------------------------------------------------
1 | describe('unit: core/syntax', () => {
2 |
3 | })
4 |
--------------------------------------------------------------------------------
/src/core/template.js:
--------------------------------------------------------------------------------
1 | const syspath = require('path')
2 | const { minify } = require('html-minifier')
3 | const { renderToString } = require('react-dom/server')
4 | const { extractCritical } = require('emotion-server')
5 | const { Helmet } = require('react-helmet')
6 | const { hijackConsole } = require('../utils/emit')
7 | const babelRequire = require('../utils/babel')
8 |
9 | function getScriptTags (entrypoints) {
10 | const files = entrypoints.main.assets
11 |
12 | return files
13 | .filter(bundle => syspath.extname(bundle) === '.js')
14 | .map(bundle => ``)
15 | .join('\n')
16 | }
17 |
18 | function templateForDevelopment (entrypoints) {
19 | return `
20 |
21 |
22 |
23 |
24 | ${getScriptTags(entrypoints)}
25 |
26 |
27 | `.trim()
28 | }
29 |
30 | function templateForProduction (entrypoints, props, route) {
31 | process.env.NODE_ENV = 'production'
32 |
33 | const hijacked = hijackConsole()
34 | const serverEntry = babelRequire('../../themes/server.js')
35 | const app = serverEntry.default(props, route)
36 | const rendered = extractCritical(renderToString(app))
37 |
38 | hijacked.restore()
39 |
40 | const helmet = Helmet.renderStatic()
41 | const template = `
42 |
43 |
44 |
45 | ${helmet.title.toString()}
46 | ${helmet.base.toString()}
47 | ${helmet.meta.toString()}
48 | ${helmet.link.toString()}
49 | ${helmet.style.toString()}
50 | ${helmet.script.toString()}
51 |
52 |
53 |
54 |
55 | ${helmet.noscript.toString()}
56 |
57 | ${rendered.html}
58 |
59 |
60 | ${getScriptTags(entrypoints)}
61 |
62 |
63 | `
64 |
65 | return minify(template, {
66 | minifyCSS: true,
67 | collapseWhitespace: true,
68 | removeComments: true,
69 | })
70 | }
71 |
72 | module.exports = {
73 | getScriptTags,
74 | templateForDevelopment,
75 | templateForProduction,
76 | }
77 |
--------------------------------------------------------------------------------
/src/core/template.spec.js:
--------------------------------------------------------------------------------
1 | const { expect } = require('code')
2 | const template = require('./template')
3 |
4 | describe('unit: core/template', () => {
5 | const entries = { main: { assets: ['file1.js', 'file2.js', 'file2.js.map'] } }
6 |
7 | it('getScriptTags()', async () => {
8 | expect(template.getScriptTags(entries)).to.equal('\n')
9 | })
10 |
11 | it('templateForDevelopment()', async () => {
12 | expect(template.templateForDevelopment(entries)).to.match(/gitdocs-app/)
13 | })
14 |
15 | it('templateForProduction()')
16 | })
17 |
--------------------------------------------------------------------------------
/src/index.js:
--------------------------------------------------------------------------------
1 | const getArguments = require('./utils/arguments')
2 | const { getConfig } = require('./utils/config')
3 | const { error } = require('./utils/emit')
4 |
5 | module.exports = async () => {
6 | try {
7 | const args = getArguments()
8 | const config = await getConfig(args.config)
9 |
10 | switch (args.cmd) {
11 | case 'init':
12 | await require('./cmds/init')(args, config)
13 | break
14 |
15 | case 'serve':
16 | case 'start':
17 | await require('./cmds/serve')(args, config)
18 | break
19 |
20 | case 'build':
21 | await require('./cmds/build')(args, config)
22 | break
23 |
24 | case 'manifest':
25 | await require('./cmds/manifest')(args, config)
26 | break
27 |
28 | case 'version':
29 | await require('./cmds/version')(args, config)
30 | break
31 |
32 | case 'help':
33 | await require('./cmds/help')(args, config)
34 | break
35 |
36 | default:
37 | throw new Error(`"${args._[0]}" is not a valid gitdocs command`)
38 | }
39 | } catch (err) {
40 | error(err, true)
41 | }
42 | }
43 |
--------------------------------------------------------------------------------
/src/sources/git.js:
--------------------------------------------------------------------------------
1 | const fs = require('fs-extra')
2 | const syspath = require('path')
3 | const simpleGit = require('simple-git/promise')
4 | const { namespaces } = require('../utils/temp')
5 |
6 | module.exports = async (dir, repo, branch, root) => {
7 | const base = syspath.basename(repo)
8 | const repoDir = syspath.join(dir, namespaces.repos, base, branch)
9 | const docsDir = syspath.join(repoDir, root)
10 |
11 | try {
12 | await simpleGit()
13 | .silent(true)
14 | .clone(repo, repoDir, { '--depth': 1 })
15 |
16 | await simpleGit(repoDir)
17 | .silent(true)
18 | .checkout(branch)
19 | } catch (err) {
20 | throw new Error(`Could not find branch in ${repo}: ${branch}`)
21 | }
22 |
23 | if (!await fs.pathExists(docsDir)) {
24 | throw new Error(`Could not find ${root}/ in ${repo}`)
25 | }
26 |
27 | return docsDir
28 | }
29 |
--------------------------------------------------------------------------------
/src/sources/local.js:
--------------------------------------------------------------------------------
1 | const fs = require('fs-extra')
2 | const syspath = require('path')
3 |
4 | module.exports = async (source, path) => {
5 | const currentDir = syspath.dirname(path)
6 | const localPath = syspath.resolve(currentDir, source)
7 |
8 | if (!await fs.pathExists(localPath)) {
9 | throw new Error(`Could not find local source: ${localPath}`)
10 | }
11 |
12 | return localPath
13 | }
14 |
--------------------------------------------------------------------------------
/src/utils/arguments.js:
--------------------------------------------------------------------------------
1 | const minimist = require('minimist')
2 |
3 | module.exports = (opts = {}) => {
4 | const argv = minimist(process.argv.slice(2), {
5 | boolean: [
6 | 'help',
7 | 'version',
8 | ],
9 | alias: {
10 | config: ['c'],
11 | help: ['h'],
12 | version: ['v'],
13 | name: ['n'],
14 | },
15 | })
16 |
17 | argv.cmd = argv.version || argv.v
18 | ? 'version'
19 | : argv.help || argv.h || !argv._[0]
20 | ? 'help'
21 | : argv._[0]
22 |
23 | return argv
24 | }
25 |
--------------------------------------------------------------------------------
/src/utils/arguments.spec.js:
--------------------------------------------------------------------------------
1 | const { expect } = require('code')
2 | const argv = require('./arguments')
3 |
4 | describe('unit: utils/arguments', () => {
5 | describe('default()', () => {
6 | it('version', async () => {
7 | process.argv = ['', '', '-v']
8 | const res = argv()
9 | expect(res.v).to.be.true()
10 | expect(res.version).to.be.true()
11 | expect(res.cmd).to.equal('version')
12 | })
13 |
14 | it('version boolean', async () => {
15 | process.argv = ['', '', '--version', 'foo']
16 | const res = argv()
17 | expect(res.version).to.be.true()
18 | })
19 |
20 | it('help', async () => {
21 | process.argv = ['', '', '-h']
22 | const res = argv()
23 | expect(res.h).to.be.true()
24 | expect(res.help).to.be.true()
25 | expect(res.cmd).to.equal('help')
26 | })
27 |
28 | it('help command', async () => {
29 | process.argv = ['', '', 'foo', '-h']
30 | const res = argv()
31 | expect(res.cmd).to.equal('help')
32 | })
33 |
34 | it('default to help', async () => {
35 | process.argv = ['', '']
36 | const res = argv()
37 | expect(res.cmd).to.equal('help')
38 | })
39 |
40 | it('command', async () => {
41 | process.argv = ['', '', 'foo', 'bar']
42 | const res = argv()
43 | expect(res.cmd).to.equal('foo')
44 | })
45 | })
46 | })
47 |
--------------------------------------------------------------------------------
/src/utils/babel.js:
--------------------------------------------------------------------------------
1 | function babelOptions (forServer) {
2 | return {
3 | babelrc: false,
4 | presets: [
5 | require.resolve('babel-preset-env'),
6 | require.resolve('babel-preset-react'),
7 | ],
8 | plugins: [
9 | require.resolve('babel-plugin-transform-runtime'),
10 | require.resolve('babel-plugin-transform-object-rest-spread'),
11 | require.resolve('babel-plugin-transform-class-properties'),
12 | ].filter(Boolean),
13 | }
14 | }
15 |
16 | function babelRequire (path) {
17 | const opts = babelOptions(true)
18 | opts.ignore = false
19 |
20 | require('babel-register')(opts)
21 | return require(path)
22 | }
23 |
24 | module.exports = babelRequire
25 | module.exports.babelOptions = babelOptions
26 |
--------------------------------------------------------------------------------
/src/utils/babel.spec.js:
--------------------------------------------------------------------------------
1 | describe('unit: utils/babel', () => {
2 | it('getMod()')
3 | it('babelRequire()')
4 | it('babelOptions')
5 | })
6 |
--------------------------------------------------------------------------------
/src/utils/config.js:
--------------------------------------------------------------------------------
1 | const fs = require('fs-extra')
2 | const syspath = require('path')
3 | const { tempDir } = require('./temp')
4 | const { addToNodePath } = require('./system')
5 | const deepmerge = require('deepmerge')
6 | const { warn } = require('./emit')
7 |
8 | const FILENAMES = [
9 | '.gitdocs.json',
10 | '.gitdocs.js',
11 | ]
12 |
13 | const JSON_FORMAT = {
14 | spaces: 2,
15 | }
16 |
17 | const DEFAULT_CONFIG = {
18 | name: 'GitDocs',
19 | root: '.',
20 | output: '.gitdocs_build',
21 | static: '.static',
22 | temp: tempDir(),
23 | logo: null,
24 | baseURL: '/',
25 | domain: '',
26 | crawlable: true,
27 | host: 'localhost',
28 | port: 8000,
29 | languages: ['bash', 'json'],
30 | header_links: [],
31 | theme: 'default',
32 | breadcrumbs: true,
33 | prefix_titles: false,
34 | table_of_contents: {
35 | page: true,
36 | folder: true,
37 | },
38 | syntax: {
39 | theme: 'atom-one-light',
40 | renderer: 'hljs',
41 | lineNumbers: true,
42 | },
43 | }
44 |
45 | function getConfigFilename () {
46 | return FILENAMES.find(fs.pathExistsSync)
47 | }
48 |
49 | function readConfigFile (file) {
50 | const ext = syspath.extname(file)
51 |
52 | return ext === '.js'
53 | ? require(syspath.resolve(file))
54 | : fs.readJson(file)
55 | }
56 |
57 | function getExternalConfigFilename (dir, name) {
58 | return FILENAMES
59 | .map(f => `${dir}/${name}/${f}`)
60 | .find(fs.pathExistsSync)
61 | }
62 |
63 | function getExternalConfig (dir, name) {
64 | const file = getExternalConfigFilename(dir, name)
65 | return file ? readConfigFile(file) : {}
66 | }
67 |
68 | async function getConfig (customFile) {
69 | // prioritize custom config file if passed,
70 | // but still fallback to default files
71 | if (customFile) {
72 | FILENAMES.unshift(customFile)
73 |
74 | if (!await fs.pathExists(customFile)) {
75 | throw new Error(`Config file was not found: ${customFile}`)
76 | }
77 | }
78 |
79 | const configFile = getConfigFilename()
80 | const userConfig = configFile ? await readConfigFile(configFile) : {}
81 | const masterConfig = deepmerge(DEFAULT_CONFIG, userConfig)
82 |
83 | masterConfig.temp = syspath.resolve(masterConfig.temp)
84 | await fs.emptyDir(masterConfig.temp)
85 | addToNodePath(masterConfig.temp)
86 |
87 | const { root } = masterConfig
88 | if (/^\//.test(root)) {
89 | warn(`Root is set to an absolute path! Did you mean ".${root}" instead of "${root}"?`)
90 | }
91 |
92 | masterConfig.static = syspath.resolve(
93 | masterConfig.root,
94 | masterConfig.static,
95 | )
96 |
97 | return masterConfig
98 | }
99 |
100 | async function createConfig (name, root) {
101 | if (getConfigFilename()) {
102 | throw new Error('GitDocs is already initialized in this folder!')
103 | }
104 |
105 | const newConfig = {
106 | name,
107 | root,
108 | }
109 |
110 | const configFile = FILENAMES[0]
111 | await fs.outputJson(configFile, newConfig, JSON_FORMAT)
112 |
113 | return configFile
114 | }
115 |
116 | module.exports = {
117 | getConfig,
118 | getExternalConfig,
119 | createConfig,
120 | }
121 |
--------------------------------------------------------------------------------
/src/utils/config.spec.js:
--------------------------------------------------------------------------------
1 | describe('unit: utils/config', () => {
2 | it('getConfig()')
3 | it('getExternalConfig()')
4 | it('createConfig()')
5 | })
6 |
--------------------------------------------------------------------------------
/src/utils/emit.js:
--------------------------------------------------------------------------------
1 | const chalk = require('chalk')
2 | const Progress = require('progress')
3 | const exitHook = require('exit-hook')
4 |
5 | const styles = {
6 | info: chalk,
7 | note: chalk.magenta,
8 | subnote: chalk.dim.italic,
9 | title: chalk.bold.underline,
10 | url: chalk.magenta.underline,
11 | warn: chalk.yellow.bold,
12 | error: chalk.dim.bold,
13 | critical: chalk.red.bold,
14 | inactive: chalk.dim,
15 | header: chalk.inverse.bold,
16 | }
17 |
18 | const chars = {
19 | CHAR_BAR: '\u2501',
20 | CHAR_PRE: styles.inactive('\u276F'),
21 | LOGO: styles.header(' GitDocs '),
22 | }
23 |
24 | function hideCursor () {
25 | process.stderr.write('\u001b[?25l')
26 | exitHook(() => process.stderr.write('\u001b[?25h'))
27 | }
28 |
29 | function fullScreen () {
30 | process.stderr.write('\x1Bc')
31 |
32 | hideCursor()
33 | process.stderr.write(`\n ${chars.LOGO}\n`)
34 | }
35 |
36 | function log (msg, firstLine) {
37 | const pre = `${firstLine ? '\n' : ''}${chars.CHAR_PRE}`
38 | process.stdout.write(styles.info(`${pre} ${msg}\n`))
39 | }
40 |
41 | function warn (msg) {
42 | const pre = styles.warn(chars.CHAR_PRE)
43 | process.stderr.write(`${pre} ${styles.warn(`Warning: ${msg}\n`)}`)
44 | }
45 |
46 | function error (err, exit) {
47 | const pre = styles.critical(chars.CHAR_PRE)
48 | err
49 | ? err.name !== 'Error' && err.stack
50 | ? process.stderr.write(`${pre} ${styles.error(err.stack)}`)
51 | : process.stderr.write(`${pre} ${styles.critical(err.message || err)}`)
52 | : process.stderr.write(`${pre} ${styles.critical('An unknown error occurred!')}`)
53 |
54 | exit && process.exit(1)
55 | }
56 |
57 | function progress (opts = {}) {
58 | const prepend = opts.prepend ? `${opts.prepend} ` : ''
59 | const append = opts.append ? ` ${opts.append}` : ''
60 | const bar = styles.info(` ${prepend}:bar${append}`)
61 |
62 | const progressBar = new Progress(bar, {
63 | width: Math.floor(process.stdout.columns / 3),
64 | incomplete: styles.inactive(chars.CHAR_BAR),
65 | complete: chars.CHAR_BAR,
66 | total: (opts.total || 0) + 1,
67 | clear: opts.clear,
68 | })
69 |
70 | // make sure it shows up immediately
71 | progressBar.tick()
72 |
73 | return progressBar
74 | }
75 |
76 | function hijackConsole () {
77 | const _log = console.log
78 | const _warn = console.warn
79 | const _error = console.error
80 |
81 | console.log = log
82 | console.warn = warn
83 | console.error = error
84 |
85 | return {
86 | restore: () => {
87 | console.log = _log
88 | console.warn = _warn
89 | console.error = _error
90 | },
91 | }
92 | }
93 |
94 | module.exports = {
95 | styles,
96 | chars,
97 | hideCursor,
98 | fullScreen,
99 | log,
100 | warn,
101 | error,
102 | progress,
103 | hijackConsole,
104 | }
105 |
--------------------------------------------------------------------------------
/src/utils/emit.spec.js:
--------------------------------------------------------------------------------
1 | describe('unit: utils/emit', () => {
2 | it('styles')
3 | it('chars')
4 | it('hideCursor()')
5 | it('fullScreen()')
6 | it('log()')
7 | it('warn()')
8 | it('error()')
9 | it('progress()')
10 | it('hijackConsole()')
11 | })
12 |
--------------------------------------------------------------------------------
/src/utils/frontmatter.js:
--------------------------------------------------------------------------------
1 | const fs = require('fs-extra')
2 | const readline = require('readline')
3 | const matter = require('gray-matter')
4 |
5 | const DELIMITER_OPEN = '---'
6 | const DELIMITER_CLOSE = '---'
7 |
8 | function parseFrontmatter (str) {
9 | const content = typeof str !== 'string'
10 | ? str.toString('utf8')
11 | : str
12 |
13 | return matter(content.trim(), {
14 | language: 'yaml',
15 | delimiters: [DELIMITER_OPEN, DELIMITER_CLOSE],
16 | })
17 | }
18 |
19 | /**
20 | * only read file until end of the front matter.
21 | * prevents having to read an entire file into
22 | * memory just to get the metadata.
23 | */
24 | function getFrontmatterOnly (file) {
25 | const lines = []
26 | const input = fs.createReadStream(file)
27 | const reader = readline.createInterface({ input })
28 |
29 | // whether we have found the start of the front matter block
30 | let delimSeen = false
31 |
32 | // read the file stream line by line
33 | reader.on('line', line => {
34 | // found some content in the file, but it's not front matter,
35 | // so assuming file has no front matter
36 | if (!delimSeen && line !== '' && line !== DELIMITER_OPEN) {
37 | reader.close()
38 | }
39 |
40 | // start of frontmatter was found
41 | if (line === DELIMITER_OPEN) {
42 | delimSeen = true
43 | }
44 |
45 | // end of frontmatter was found
46 | if (line === DELIMITER_CLOSE && delimSeen) {
47 | reader.close()
48 | }
49 |
50 | lines.push(line)
51 | })
52 |
53 | return new Promise((resolve, reject) => {
54 | reader.on('error', err => reject(err))
55 | reader.on('close', () => input.close())
56 |
57 | // done reading the front matter and read stream has been closed
58 | input.on('close', () => {
59 | // concat the lines into a string to be parsed into an object
60 | const fm = lines.join('\n').trim()
61 | const { data } = parseFrontmatter(fm)
62 |
63 | resolve(data)
64 | })
65 | })
66 | }
67 |
68 | module.exports = {
69 | parseFrontmatter,
70 | getFrontmatterOnly,
71 | }
72 |
--------------------------------------------------------------------------------
/src/utils/frontmatter.spec.js:
--------------------------------------------------------------------------------
1 | const { expect } = require('code')
2 | const mockFs = require('mock-fs')
3 | const fs = require('fs')
4 | const frontmatter = require('./frontmatter')
5 |
6 | describe('unit: utils/frontmatter', () => {
7 | afterEach(mockFs.restore)
8 | beforeEach(() => mockFs({
9 | 'file1.md': `---
10 | foo: bar
11 | baz: qux
12 | ---
13 | # Hello There
14 | `,
15 | 'file2.md': `
16 | ---
17 | foo: bar
18 | ---
19 | # Hi
20 | `,
21 | 'file3.md': `
22 | # Hello There
23 | `,
24 | }))
25 |
26 | describe('parseFrontmatter()', () => {
27 | it('normal', async () => {
28 | const res = await frontmatter.parseFrontmatter(fs.readFileSync('file1.md'))
29 | expect(res.data).to.equal({ foo: 'bar', baz: 'qux' })
30 | expect(res.content).to.equal('# Hello There')
31 | })
32 |
33 | it('with whitespace', async () => {
34 | const res = await frontmatter.parseFrontmatter(fs.readFileSync('file2.md'))
35 | expect(res.data).to.equal({ foo: 'bar' })
36 | expect(res.content).to.equal('# Hi')
37 | })
38 |
39 | it('without frontmatter', async () => {
40 | const res = await frontmatter.parseFrontmatter(fs.readFileSync('file3.md'))
41 | expect(res.data).to.equal({})
42 | expect(res.content).to.equal('# Hello There')
43 | })
44 | })
45 |
46 | describe('getFrontmatterOnly()', () => {
47 | it('normal', async () => {
48 | const data = await frontmatter.getFrontmatterOnly('file1.md')
49 | expect(data).to.equal({ foo: 'bar', baz: 'qux' })
50 | })
51 |
52 | it('with whitespace', async () => {
53 | const data = await frontmatter.getFrontmatterOnly('file2.md')
54 | expect(data).to.equal({ foo: 'bar' })
55 | })
56 |
57 | it('without frontmatter', async () => {
58 | const data = await frontmatter.getFrontmatterOnly('file3.md')
59 | expect(data).to.equal({})
60 | })
61 | })
62 | })
63 |
--------------------------------------------------------------------------------
/src/utils/merge.js:
--------------------------------------------------------------------------------
1 | const { routify } = require('./path')
2 |
3 | function mergeExternalConfig (config, externals) {
4 | externals.forEach(e => {
5 | const order = Object
6 | .keys(e.config.order || {})
7 | .map(k => ({
8 | [`/${routify(e.name)}${k}`]: e.config.order[k]
9 | }))
10 | .reduce((acc, cur) => ({ ...acc, ...cur }), {})
11 | config.order = { ...order, ...config.order }
12 | })
13 |
14 | return config
15 | }
16 |
17 | function mergeLeftByKey (items1, items2, opts = {}) {
18 | const merged = items1.map(item => {
19 | const itemSrc = items2.find(i => i[opts.key] === item[opts.key])
20 |
21 | return itemSrc
22 | ? { ...item, ...itemSrc }
23 | : item
24 | })
25 |
26 | const filtered = items2.filter(i =>
27 | items1.findIndex(j => j[opts.key] === i[opts.key]) === -1)
28 |
29 | switch (opts.strategy) {
30 | case 'replace':
31 | return merged
32 |
33 | case 'prepend':
34 | return merged.concat(filtered)
35 |
36 | case 'append':
37 | return filtered.concat(merged)
38 |
39 | default:
40 | if (filtered.length) {
41 | return filtered
42 | }
43 | }
44 | }
45 |
46 | module.exports = {
47 | mergeExternalConfig,
48 | mergeLeftByKey,
49 | }
50 |
--------------------------------------------------------------------------------
/src/utils/merge.spec.js:
--------------------------------------------------------------------------------
1 | describe('unit: utils/merge', () => {
2 | it('mergeExternalConfig()')
3 | it('mergeLeftByKey()')
4 | })
5 |
--------------------------------------------------------------------------------
/src/utils/path.js:
--------------------------------------------------------------------------------
1 | const syspath = require('path')
2 | const { indexFilenames } = require('../core/filesystem')
3 |
4 | function escapeForRegex (str) {
5 | return str.replace(/[-[\]{}()*+?.,\\^$|#\s]/g, '\\$&')
6 | }
7 |
8 | function removeExt (str) {
9 | return str.replace(/\.[^/.]+$/, '')
10 | }
11 |
12 | function removeIndex (str) {
13 | const files = indexFilenames.map(removeExt).join('|')
14 | const pattern = new RegExp(`(.*)(${files})(\\.|$).*$`)
15 |
16 | return str.replace(pattern, '$1')
17 | }
18 |
19 | /**
20 | * removes leading and trailing slashes so
21 | * we can normalize them later on.
22 | */
23 | function removeSlashes (str) {
24 | return str.replace(/^\/|\/$/g, '')
25 | }
26 |
27 | /**
28 | * turns a file path into a formatted title,
29 | * removing any index files and symbols. e.g.
30 | * some-path/index.md ==> Some Path
31 | */
32 | function titlify (str) {
33 | // trim stuff from the end of the string
34 | const trimmed = removeIndex(removeExt(str))
35 |
36 | // capitalize each word of the filename
37 | return syspath.basename(trimmed)
38 | .split('-')
39 | .map(i => `${i.charAt(0).toUpperCase()}${i.substr(1)}`)
40 | .join(' ')
41 | }
42 |
43 | function routify (str, base = '') {
44 | // turn string into a slug
45 | const slug = str
46 | .trim()
47 | .toLowerCase()
48 | .replace(/ /g, '-')
49 |
50 | // trim stuff from the string
51 | const normalized = removeSlashes(removeIndex(removeExt(slug)))
52 |
53 | // wrap in leading and trailing slashes
54 | return `/${normalized}${normalized !== '' ? '/' : ''}`
55 | }
56 |
57 | module.exports = {
58 | escapeForRegex,
59 | removeExt,
60 | removeIndex,
61 | removeSlashes,
62 | titlify,
63 | routify,
64 | }
65 |
--------------------------------------------------------------------------------
/src/utils/path.spec.js:
--------------------------------------------------------------------------------
1 | const { expect } = require('code')
2 | const path = require('./path')
3 |
4 | describe('unit: utils/path', () => {
5 | it('escapeForRegex()', async () => {
6 | expect(path.escapeForRegex('foo.bar')).to.equal('foo\\.bar')
7 | })
8 |
9 | it('removeExt()', async () => {
10 | expect(path.removeExt('foo.md')).to.equal('foo')
11 | expect(path.removeExt('foo/bar.md')).to.equal('foo/bar')
12 | expect(path.removeExt('foo')).to.equal('foo')
13 | })
14 |
15 | it('removeIndex()', async () => {
16 | expect(path.removeIndex('foo/index')).to.equal('foo/')
17 | expect(path.removeIndex('foo/readme')).to.equal('foo/')
18 | expect(path.removeIndex('foo/index.md')).to.equal('foo/')
19 | expect(path.removeIndex('foo/readme.md')).to.equal('foo/')
20 | expect(path.removeIndex('foo/index-test')).to.equal('foo/index-test')
21 | expect(path.removeIndex('foo/readme-test')).to.equal('foo/readme-test')
22 | expect(path.removeIndex('foo')).to.equal('foo')
23 | expect(path.removeIndex('foo/bar')).to.equal('foo/bar')
24 | expect(path.removeIndex('foo/bar.md')).to.equal('foo/bar.md')
25 | })
26 |
27 | it('removeSlashes()', async () => {
28 | expect(path.removeSlashes('/foo/bar/')).to.equal('foo/bar')
29 | expect(path.removeSlashes('foo/bar/')).to.equal('foo/bar')
30 | expect(path.removeSlashes('/foo/bar')).to.equal('foo/bar')
31 | expect(path.removeSlashes('foo/bar')).to.equal('foo/bar')
32 | })
33 |
34 | it('titlify()', async () => {
35 | expect(path.titlify('foo')).to.equal('Foo')
36 | expect(path.titlify('foo-bar')).to.equal('Foo Bar')
37 | expect(path.titlify('foo-bar.md')).to.equal('Foo Bar')
38 | expect(path.titlify('foo/bar/baz-qux.md')).to.equal('Baz Qux')
39 | expect(path.titlify('/foo-bar/')).to.equal('Foo Bar')
40 | expect(path.titlify('/foo-bar/index')).to.equal('Foo Bar')
41 | expect(path.titlify('/foo-bar/index.md')).to.equal('Foo Bar')
42 | expect(path.titlify('/foo-bar/baz-qux')).to.equal('Baz Qux')
43 | expect(path.titlify('/foo-bar/baz-qux/')).to.equal('Baz Qux')
44 | })
45 |
46 | it('routify()', async () => {
47 | expect(path.routify('foo bar')).to.equal('/foo-bar/')
48 | expect(path.routify('Foo Bar/Baz')).to.equal('/foo-bar/baz/')
49 | expect(path.routify('foo/bar')).to.equal('/foo/bar/')
50 | expect(path.routify('foo/bar.md')).to.equal('/foo/bar/')
51 | expect(path.routify('/foo/bar')).to.equal('/foo/bar/')
52 | expect(path.routify('/foo/bar/index')).to.equal('/foo/bar/')
53 | expect(path.routify('/foo/bar/index.md')).to.equal('/foo/bar/')
54 | expect(path.routify('/foo/bar/index.html')).to.equal('/foo/bar/')
55 | expect(path.routify('/foo/bar/baz.md')).to.equal('/foo/bar/baz/')
56 | expect(path.routify('/foo/bar/')).to.equal('/foo/bar/')
57 | expect(path.routify('foo')).to.equal('/foo/')
58 | expect(path.routify('/')).to.equal('/')
59 | expect(path.routify('')).to.equal('/')
60 | })
61 | })
62 |
--------------------------------------------------------------------------------
/src/utils/port.js:
--------------------------------------------------------------------------------
1 | const net = require('net')
2 |
3 | module.exports = () => {
4 | const server = net.createServer()
5 |
6 | return new Promise((resolve, reject) => {
7 | server.on('error', reject)
8 |
9 | server.unref()
10 | server.listen(0, () => {
11 | const port = server.address().port
12 | server.close(() => resolve(port))
13 | })
14 | })
15 | }
16 |
--------------------------------------------------------------------------------
/src/utils/port.spec.js:
--------------------------------------------------------------------------------
1 | describe('unit: utils/port', () => {
2 | it('default()')
3 | })
4 |
--------------------------------------------------------------------------------
/src/utils/promise.js:
--------------------------------------------------------------------------------
1 | async function concurrentChunks (concurrency, items) {
2 | const chunked = []
3 |
4 | for (let i = 0; i < items.length; i += concurrency) {
5 | const chunk = items.slice(i, i + concurrency)
6 |
7 | const result = await Promise.all(
8 | chunk.map(item => {
9 | return typeof item === 'function'
10 | ? item()
11 | : item
12 | })
13 | )
14 |
15 | result.forEach(res => chunked.push(res))
16 | }
17 |
18 | return chunked
19 | }
20 |
21 | module.exports = {
22 | concurrentChunks,
23 | }
24 |
25 | // var Worker = require('webworker-threads').Worker;
26 | // // var w = new Worker('worker.js'); // Standard API
27 |
28 | // // You may also pass in a function:
29 | // var worker = new Worker(function(){
30 | // postMessage("I'm working before postMessage('ali').");
31 | // this.onmessage = function(event) {
32 | // postMessage('Hi ' + event.data);
33 | // self.close();
34 | // };
35 | // });
36 | // worker.onmessage = function(event) {
37 | // console.log("Worker said : " + event.data);
38 | // };
39 | // worker.postMessage('ali');
40 |
41 | // const cluster = require('cluster');
42 | // const http = require('http');
43 | // const numCPUs = require('os').cpus().length;
44 |
45 | // if (cluster.isMaster) {
46 | // // Fork workers.
47 | // for (var i = 0; i < numCPUs; i++) {
48 | // cluster.fork();
49 | // }
50 |
51 | // cluster.on('exit', (worker, code, signal) => {
52 | // console.log(`worker ${worker.process.pid} died`);
53 | // });
54 | // } else {
55 | // // Workers can share any TCP connection
56 | // // In this case it is an HTTP server
57 | // http.createServer((req, res) => {
58 | // res.writeHead(200);
59 | // res.end('hello world\n');
60 | // }).listen(8000);
61 | // }
62 |
--------------------------------------------------------------------------------
/src/utils/promise.spec.js:
--------------------------------------------------------------------------------
1 | const { expect } = require('code')
2 | const promise = require('./promise')
3 |
4 | describe('unit: utils/promise', () => {
5 | it('concurrentChunks()', async () => {
6 | const promises = [
7 | () => new Promise(resolve => setTimeout(() => resolve(1), 100)),
8 | () => new Promise(resolve => setTimeout(() => resolve(2), 100)),
9 | () => new Promise(resolve => setTimeout(() => resolve(3), 100)),
10 | () => new Promise(resolve => setTimeout(() => resolve(4), 100)),
11 | () => new Promise(resolve => setTimeout(() => resolve(5), 100)),
12 | () => new Promise(resolve => setTimeout(() => resolve(6), 100)),
13 | ]
14 |
15 | const before = Date.now()
16 | const res = await promise.concurrentChunks(2, promises)
17 | const after = Date.now()
18 | const diff = after - before
19 |
20 | expect(diff > 300 && diff < 400).to.be.true()
21 | expect(res).to.equal([1, 2, 3, 4, 5, 6])
22 | })
23 | })
24 |
--------------------------------------------------------------------------------
/src/utils/readline.js:
--------------------------------------------------------------------------------
1 | const readline = require('readline')
2 | const { chars, styles } = require('./emit')
3 |
4 | const _getInterface = () =>
5 | readline.createInterface({
6 | input: process.stdin,
7 | output: process.stdout,
8 | })
9 |
10 | function _getQuestion (q, hint) {
11 | return `${chars.CHAR_PRE} ${q}${hint ? styles.inactive(` (${hint})`) : ''} `
12 | }
13 |
14 | function ask (question, opts = {}) {
15 | return new Promise((resolve, reject) => {
16 | const iface = _getInterface()
17 | const message = _getQuestion(question, opts.default)
18 |
19 | iface.question(message, answer => {
20 | iface.close()
21 | resolve(answer || opts.default)
22 | })
23 | })
24 | }
25 |
26 | function confirm (question, opts = {}) {
27 | return new Promise((resolve, reject) => {
28 | const iface = _getInterface()
29 | const message = _getQuestion(question, 'y/n')
30 |
31 | iface.question(message, answer => {
32 | iface.close()
33 | resolve(/^y/i.test(answer || opts.default))
34 | })
35 | })
36 | }
37 |
38 | module.exports = {
39 | ask,
40 | confirm,
41 | }
42 |
--------------------------------------------------------------------------------
/src/utils/readline.spec.js:
--------------------------------------------------------------------------------
1 | describe('unit: utils/readline', () => {
2 | it('ask()')
3 | it('confirm()')
4 | })
5 |
--------------------------------------------------------------------------------
/src/utils/system.js:
--------------------------------------------------------------------------------
1 | const syspath = require('path')
2 | const { Module } = require('module')
3 |
4 | function addToNodePath (path) {
5 | const currentPath = process.env.NODE_PATH || ''
6 |
7 | process.env.NODE_PATH = currentPath
8 | .split(syspath.delimiter)
9 | .filter(Boolean)
10 | .concat(path)
11 | .join(syspath.delimiter)
12 |
13 | if (typeof Module._initPaths !== 'function') {
14 | throw new Error('Module._initPaths is not available in this version of Node!')
15 | }
16 |
17 | Module._initPaths()
18 | }
19 |
20 | module.exports = {
21 | addToNodePath,
22 | }
23 |
--------------------------------------------------------------------------------
/src/utils/system.spec.js:
--------------------------------------------------------------------------------
1 | describe('unit: utils/system', () => {
2 | it('addToNodePath()')
3 | })
4 |
--------------------------------------------------------------------------------
/src/utils/temp.js:
--------------------------------------------------------------------------------
1 | const tmp = require('tmp')
2 |
3 | const namespaces = {
4 | codegen: '@codegen',
5 | static: '@static',
6 | repos: '@repos',
7 | }
8 |
9 | function tempDir () {
10 | tmp.setGracefulCleanup()
11 |
12 | const dir = tmp.dirSync({
13 | prefix: 'gitdocs-',
14 | })
15 |
16 | return dir.name
17 | }
18 |
19 | module.exports = {
20 | namespaces,
21 | tempDir,
22 | }
23 |
--------------------------------------------------------------------------------
/src/utils/temp.spec.js:
--------------------------------------------------------------------------------
1 | describe('unit: utils/temp', () => {
2 | it('namespaces()')
3 | it('tempDir()')
4 | })
5 |
--------------------------------------------------------------------------------
/src/utils/theme.js:
--------------------------------------------------------------------------------
1 | // const path = require('path')
2 | // const resolve = require('resolve')
3 |
4 | // const PACKAGE_PREFIX = 'gitdocs-theme'
5 |
6 | // function getTheme (name) {
7 | // return new Promise((done, reject) => {
8 | // const opts = {
9 | // // basedir: process.cwd(),
10 | // paths: [
11 | // process.cwd(),
12 | // path.resolve(__dirname, '../../'),
13 | // ]
14 | // }
15 |
16 | // resolve(`${PACKAGE_PREFIX}-${name}`, opts, (err, res) => {
17 | // if (err) reject(err)
18 | // else done(res)
19 | // })
20 | // })
21 | // }
22 |
--------------------------------------------------------------------------------
/src/utils/theme.spec.js:
--------------------------------------------------------------------------------
1 | describe('unit: utils/theme', () => {
2 |
3 | })
4 |
--------------------------------------------------------------------------------
/starter/about-us/readme.md:
--------------------------------------------------------------------------------
1 | # About Us
2 |
3 | GitDocs was built by the good folks over here at [Timber](https://timber.io). You should check us out.
4 |
--------------------------------------------------------------------------------
/starter/about-us/work-with-us.md:
--------------------------------------------------------------------------------
1 | # Work With Us
2 |
3 | Want to build cool things? [Timber Careers](https://timber.io/about)
4 |
--------------------------------------------------------------------------------
/starter/getting-started/installation.md:
--------------------------------------------------------------------------------
1 | # Installation
2 |
3 | Here are some installation instructions for our thing.
4 |
--------------------------------------------------------------------------------
/starter/getting-started/readme.md:
--------------------------------------------------------------------------------
1 | # Getting Started
2 |
3 | Here are some instructions on how to get started with our thing.
4 |
--------------------------------------------------------------------------------
/starter/getting-started/troubleshooting.md:
--------------------------------------------------------------------------------
1 | # Troubleshooting
2 |
3 | Having problems with the thing? Deal with them yourself.
4 |
--------------------------------------------------------------------------------
/starter/readme.md:
--------------------------------------------------------------------------------
1 | # Welcome to GitDocs
2 |
3 | This is a basic documentation site to get you started using GitDocs! We hope you enjoy it.
4 |
--------------------------------------------------------------------------------
/tests/help.test.js:
--------------------------------------------------------------------------------
1 | const { expect } = require('code')
2 | const { run } = require('./helpers')
3 |
4 | describe('integration: help', () => {
5 | it('shows help menu with command', async () => {
6 | const res = await run('help')
7 | expect(res.stdout).to.match(/usage/)
8 | })
9 |
10 | it('shows help submenu with command', async () => {
11 | const res = await run('help build')
12 | expect(res.stdout).to.match(/gitdocs build/)
13 | })
14 |
15 | it('shows help submenu with flag', async () => {
16 | const res = await run('build -h')
17 | expect(res.stdout).to.match(/gitdocs build/)
18 | })
19 | })
20 |
--------------------------------------------------------------------------------
/tests/helpers.js:
--------------------------------------------------------------------------------
1 | const path = require('path')
2 | const execa = require('execa')
3 |
4 | async function run (cmd, opts = {}) {
5 | const gitdocs = path.resolve(__dirname, '../bin/gitdocs')
6 | return execa(gitdocs, cmd.split(' '), opts)
7 | }
8 |
9 | module.exports = {
10 | run,
11 | }
12 |
--------------------------------------------------------------------------------
/tests/index.test.js:
--------------------------------------------------------------------------------
1 | const { expect } = require('code')
2 | const { run } = require('./helpers')
3 |
4 | describe('integration: index', () => {
5 | it('defaults to help command', async () => {
6 | const res = await run('')
7 | expect(res.stdout).to.match(/usage/)
8 | })
9 |
10 | it('shows package version', async () => {
11 | const res = await run('-v')
12 | expect(res.stdout).to.match(/v[0-9]/)
13 | })
14 |
15 | it('throws on invalid command', async () => {
16 | const res = await run('foobar', { reject: false })
17 | expect(res.code).to.equal(1)
18 | expect(res.stderr).to.match(/not a valid/)
19 | })
20 | })
21 |
--------------------------------------------------------------------------------
/tests/manifest.test.js:
--------------------------------------------------------------------------------
1 | const syspath = require('path')
2 | const { expect } = require('code')
3 | const { run } = require('./helpers')
4 |
5 | describe('integration: manifest', () => {
6 | const cwd = `${__dirname}/mock`
7 |
8 | it('generates correct manifest object', async () => {
9 | const { stdout } = await run('manifest', { cwd })
10 | const res = JSON.parse(stdout)
11 | expect(res.path).to.equal('')
12 | expect(res.title).to.equal('Mock')
13 | expect(res.description).to.be.undefined()
14 | expect(res.url).to.equal('/')
15 | expect(res.input).to.equal(syspath.resolve(__dirname, 'mock/readme.md'))
16 | expect(res.outputDir).to.equal('.gitdocs_build/')
17 | expect(res.toc.page).to.have.length(1)
18 | expect(res.toc.folder).to.have.length(5)
19 | expect(res.toc.folder[0].title).to.equal('The Foo')
20 | expect(res.toc.folder[0].description).to.equal('This is a test')
21 | expect(res.toc.folder[0].url).to.equal('/foo/')
22 | expect(res.breadcrumbs).to.have.length(1)
23 | expect(res.breadcrumbs[0].title).to.equal('Mock')
24 | expect(res.breadcrumbs[0].url).to.equal('/')
25 | expect(res.items).to.have.length(7)
26 | expect(res.items[0].path).to.equal('foo')
27 | expect(res.items[0].title).to.equal('The Foo')
28 | expect(res.items[0].description).to.equal('This is a test')
29 | expect(res.items[0].url).to.equal('/foo/')
30 | expect(res.items[0].input).to.equal(syspath.resolve(__dirname, 'mock/foo/index.md'))
31 | expect(res.items[0].outputDir).to.equal('.gitdocs_build/foo/')
32 | expect(res.items[0].breadcrumbs).to.have.length(2)
33 | expect(res.items[0].breadcrumbs[0].title).to.equal('Mock')
34 | expect(res.items[0].breadcrumbs[0].url).to.equal('/')
35 | expect(res.items[0].breadcrumbs[1].title).to.equal('The Foo')
36 | expect(res.items[0].breadcrumbs[1].url).to.equal('/foo/')
37 | expect(res.items[0].items).to.have.length(3)
38 | expect(res.items[1].title).to.equal('Garply')
39 | expect(res.items[1].items[1].hidden).to.be.true()
40 | expect(res.items[2].title).to.equal('XYZZY')
41 | expect(res.items[2].items[0].draft).to.be.true()
42 | expect(res.items[3].title).to.equal('Thud')
43 | expect(res.items[3].items[0].draft).to.be.true()
44 | expect(res.items[4].path).to.equal('external.md')
45 | expect(res.items[4].title).to.equal('GitDocs')
46 | expect(res.items[4].url).to.equal('/gitdocs/')
47 | expect(res.items[4].input).to.match(/\/@repos\/gitdocs/)
48 | expect(res.items[4].outputDir).to.equal('.gitdocs_build/gitdocs/')
49 | expect(res.items[4].breadcrumbs).to.have.length(2)
50 | expect(res.items[4].breadcrumbs[0].title).to.equal('Mock')
51 | expect(res.items[4].breadcrumbs[0].url).to.equal('/')
52 | expect(res.items[4].breadcrumbs[1].title).to.equal('GitDocs')
53 | expect(res.items[4].breadcrumbs[1].url).to.equal('/gitdocs/')
54 | expect(res.items[4].items).to.have.length(15)
55 | expect(res.items[4].items[0].path).to.equal('installation.md')
56 | expect(res.items[4].items[0].title).to.equal('Installation')
57 | expect(res.items[4].items[0].url).to.equal('/gitdocs/installation/')
58 | expect(res.items[5].component).to.equal('Divider')
59 | expect(res.items[6].title).to.equal('The Quux')
60 | expect(res.items[6].items[0].url).to.equal('/qux/grault/')
61 | expect(res.items[6].items[1].url).to.equal('/qux/corge/')
62 | expect(res.items[6].items[0].breadcrumbs).to.have.length(3)
63 | expect(res.items[6].items[0].breadcrumbs[0].title).to.equal('Mock')
64 | expect(res.items[6].items[0].breadcrumbs[1].title).to.equal('The Quux')
65 | expect(res.items[6].items[0].breadcrumbs[2].title).to.equal('The Grault?')
66 | expect(res.items[6].items[1].breadcrumbs).to.be.undefined()
67 | })
68 | })
69 |
--------------------------------------------------------------------------------
/tests/mock/.gitdocs.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "Mock",
3 | "root": "."
4 | }
5 |
--------------------------------------------------------------------------------
/tests/mock/external.md:
--------------------------------------------------------------------------------
1 | ---
2 | source: https://github.com/timberio/gitdocs
3 | source_type: git
4 | source_branch: master
5 | title: GitDocs
6 | url: gitdocs
7 | ---
8 |
--------------------------------------------------------------------------------
/tests/mock/foo/bar.md:
--------------------------------------------------------------------------------
1 | ---
2 | title: The Bar
3 | ---
4 | # The Bar
5 |
--------------------------------------------------------------------------------
/tests/mock/foo/baz.md:
--------------------------------------------------------------------------------
1 | ---
2 | title: The Baz
3 | ---
4 | # The Baz
5 |
--------------------------------------------------------------------------------
/tests/mock/foo/index.md:
--------------------------------------------------------------------------------
1 | ---
2 | title: The Foo
3 | description: This is a test
4 | items_append:
5 | - component: Divider
6 | - path: bar.md
7 | title: The Best Bar
8 | ---
9 | # The Foo
10 |
--------------------------------------------------------------------------------
/tests/mock/garply/fred.md:
--------------------------------------------------------------------------------
1 | ---
2 | hidden: true
3 | ---
4 | # The Fred
5 |
--------------------------------------------------------------------------------
/tests/mock/garply/readme.md:
--------------------------------------------------------------------------------
1 | ---
2 | items:
3 | - waldo.md
4 | - fred.md
5 | ---
6 | # The Garply
7 |
--------------------------------------------------------------------------------
/tests/mock/garply/waldo.md:
--------------------------------------------------------------------------------
1 | # The Waldo
2 |
--------------------------------------------------------------------------------
/tests/mock/plugh.md:
--------------------------------------------------------------------------------
1 | # The Plugh
2 |
--------------------------------------------------------------------------------
/tests/mock/qux/corge.md:
--------------------------------------------------------------------------------
1 | ---
2 | breadcrumbs: false
3 | ---
4 | # The Corge
5 |
--------------------------------------------------------------------------------
/tests/mock/qux/grault.md:
--------------------------------------------------------------------------------
1 | ---
2 | items:
3 | - title: Timber
4 | url: https://timber.io
5 | ---
6 | # The Grault
7 |
--------------------------------------------------------------------------------
/tests/mock/readme.md:
--------------------------------------------------------------------------------
1 | ---
2 | items:
3 | - foo
4 | - garply
5 | - xyzzy
6 | - thud.md
7 | - external.md
8 | - component: Divider
9 | - path: qux
10 | title: The Quux
11 | items_prepend:
12 | - path: grault.md
13 | title: The Grault?
14 | ---
15 | # Welcome to Mock
16 |
--------------------------------------------------------------------------------
/tests/mock/thud.md:
--------------------------------------------------------------------------------
1 | ---
2 | source: ./xyzzy
3 | source_type: local
4 | ---
5 |
--------------------------------------------------------------------------------
/tests/mock/xyzzy/index.md:
--------------------------------------------------------------------------------
1 | ---
2 | title: XYZZY
3 | items_prepend:
4 | - path: zzz.md
5 | draft: true
6 | ---
7 | # The Xyzzy
8 |
--------------------------------------------------------------------------------
/tests/mock/xyzzy/zz.md:
--------------------------------------------------------------------------------
1 | # The ZZ
2 |
--------------------------------------------------------------------------------
/tests/mock/xyzzy/zzz.md:
--------------------------------------------------------------------------------
1 | # The ZZZ
2 |
--------------------------------------------------------------------------------
/tests/mock/xyzzy/zzzz.md:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/vectordotdev/gitdocs/d9a11ab96041c94cbb216362d60e8e629b3aee2d/tests/mock/xyzzy/zzzz.md
--------------------------------------------------------------------------------
/themes/default/application/index.js:
--------------------------------------------------------------------------------
1 | import React, { Component } from 'react'
2 | import { withRouter } from 'react-router-dom'
3 | import { Theme } from '@timberio/ui'
4 | import Helmet from 'react-helmet'
5 | import raf from 'raf'
6 | import Header from '../header'
7 | import Sidebar from '../sidebar'
8 | import Page from '../page'
9 | import NotFound from '../not-found'
10 | import Routes from '../routes'
11 | import { ConfigContext } from '../context'
12 | import { Wrapper, WrapperNav, WrapperPage } from './styles'
13 |
14 | class App extends Component {
15 | state = { sticky: false }
16 |
17 | componentDidUpdate (prevProps) {
18 | if (this.props.location !== prevProps.location) {
19 | this._wrapper.scrollTo(0, 0)
20 | }
21 | }
22 |
23 | handleScroll = e => {
24 | if (!this.framePending) {
25 | const { currentTarget } = e
26 |
27 | raf(() => {
28 | this.framePending = false
29 | const { sticky } = this.state
30 |
31 | if (!sticky && currentTarget.scrollTop > 70) {
32 | this.setState({ sticky: true })
33 | } else if (sticky && currentTarget.scrollTop < 70) {
34 | this.setState({ sticky: false })
35 | }
36 | })
37 | this.framePending = true
38 | }
39 | }
40 |
41 | render () {
42 | const {
43 | config,
44 | manifest,
45 | } = this.props
46 |
47 | return (
48 |
49 |
50 |
51 |
55 |
56 |
57 |
61 | {manifest.description && }
65 |
69 |
70 |
71 |
72 |
76 |
77 |
78 | this._wrapper = ref}
80 | onScroll={this.handleScroll}
81 | >
82 |
86 |
87 |
96 |
97 |
98 |
99 |
100 | )
101 | }
102 | }
103 |
104 | export default withRouter(App)
105 |
--------------------------------------------------------------------------------
/themes/default/application/styles.js:
--------------------------------------------------------------------------------
1 | import { normalize } from '@timberio/ui'
2 | import styled, { css } from 'react-emotion'
3 |
4 | normalize()
5 |
6 | const fontMain = css`
7 | font-family: 'Source Sans Pro', sans-serif;
8 | font-size: 1.1rem;
9 | line-height: 1.75;
10 | color: rgba(0, 0, 0, 0.85);
11 | text-rendering: optimizeLegibility;
12 | -moz-osx-font-smoothing: grayscale;
13 | -webkit-font-smoothing: antialiased;
14 | `
15 |
16 | const fontMono = css`
17 | font-family: "Menlo", "Source Code Pro", "Inconsolata","monospace", serif;
18 | font-size: 13px;
19 | line-height: 21px;
20 | `
21 |
22 | export const Wrapper = styled('div')`
23 | display: flex;
24 | height: 100vh;
25 | @media (max-width: 850px) {
26 | flex-direction: column;
27 | }
28 | &, input {
29 | ${fontMain};
30 | }
31 | * pre, code {
32 | ${fontMono};
33 | }
34 | `
35 |
36 | export const WrapperNav = styled('nav')`
37 | flex: 1;
38 | background: linear-gradient(90deg, #F0F2F4 0%, #F5F7F9 100%);
39 | background: #FAFAFD;
40 | border-right: 1px solid #E6E9EB;
41 | box-shadow: inset -4px 0px 2px -2px rgba(202, 209, 226, 0.2);
42 | text-align: right;
43 | overflow: auto;
44 |
45 | @media (min-width: 850px) {
46 | min-width: 270px;
47 | max-width: 270px;
48 | }
49 |
50 | @media (min-width: 1480px) {
51 | max-width: initial;
52 | }
53 |
54 | @media (max-width: 850px) {
55 | flex: 0 auto;
56 | overflow: hidden;
57 | box-shadow: 0 1px 3px rgba(0, 0, 0, .2);
58 | }
59 | `
60 |
61 | export const WrapperPage = styled('div')`
62 | flex: 2;
63 | overflow: auto;
64 | position: relative;
65 | @media (max-width: 500px) {
66 | max-width: 100%;
67 | }
68 | `
69 |
--------------------------------------------------------------------------------
/themes/default/breadcrumbs/index.js:
--------------------------------------------------------------------------------
1 | import React from 'react'
2 | import PropTypes from 'prop-types'
3 | import {
4 | Wrapper,
5 | CrumbWrapper,
6 | Crumb,
7 | CrumbInactive,
8 | Seperator,
9 | } from './styles'
10 |
11 | const Breadcrumbs = (props) => {
12 | // don't show breadcrumbs if there is only one item
13 | if (props.items.length < 2) {
14 | return
15 | }
16 |
17 | return (
18 |
19 | {props.items.map((item, i) => (
20 |
21 | {i > 0 && }
22 | {item.url
23 | ? {item.title}
24 | : {item.title} }
25 |
26 | ))}
27 |
28 | )
29 | }
30 |
31 | Breadcrumbs.propTypes = {
32 | items: PropTypes.array,
33 | }
34 |
35 | Breadcrumbs.defaultProps = {
36 | items: [],
37 | }
38 |
39 | export default Breadcrumbs
40 |
--------------------------------------------------------------------------------
/themes/default/breadcrumbs/styles.js:
--------------------------------------------------------------------------------
1 | import styled from 'react-emotion'
2 | import { Link } from 'react-router-dom'
3 | import { ChevronRight } from 'react-feather'
4 |
5 | export const Wrapper = styled('nav')`
6 | margin-bottom: 20px;
7 | `
8 |
9 | export const CrumbWrapper = styled('div')`
10 | display: inline-block;
11 | `
12 |
13 | export const Crumb = styled(Link)`
14 | color: #848B8E;
15 | font-weight: 600;
16 | font-size: 1rem;
17 | text-decoration: none;
18 | opacity: .5;
19 | transition: opacity .1s;
20 | &:hover {
21 | opacity: 1;
22 | }
23 | `
24 |
25 | export const CrumbInactive = styled(Crumb.withComponent('span'))`
26 | &:hover {
27 | opacity: .5;
28 | }
29 | `
30 |
31 | export const Seperator = styled(ChevronRight)`
32 | display: inline-block;
33 | opacity: .2;
34 | padding: 0 5px;
35 | position: relative;
36 | top: 2px;
37 | `
38 |
--------------------------------------------------------------------------------
/themes/default/context.js:
--------------------------------------------------------------------------------
1 | import React from 'react'
2 |
3 | export const ConfigContext = React.createContext({})
4 |
--------------------------------------------------------------------------------
/themes/default/header/index.js:
--------------------------------------------------------------------------------
1 | import React from 'react'
2 | import { ConfigContext } from '../context'
3 | import Search from '../search'
4 | import { Wrapper, Nav } from './styles'
5 |
6 | export default function (props) {
7 | return (
8 |
9 | {config =>
10 |
11 | {!props.isSSR && }
15 |
16 | {config.header_links.map(({ title, ...rest }) => (
17 | {title}
18 | ))}
19 |
20 |
21 | }
22 |
23 | )
24 | }
25 |
--------------------------------------------------------------------------------
/themes/default/header/styles.js:
--------------------------------------------------------------------------------
1 | import styled from 'react-emotion'
2 |
3 | export const Wrapper = styled('header')`
4 | display: flex;
5 | align-items: center;
6 | justify-content: space-between;
7 | height: 70px;
8 | padding: 0 30px;
9 | border-bottom: 1px solid #E6E9EB;
10 | `
11 |
12 | export const Nav = styled('nav')`
13 | flex-shrink: 0;
14 | a {
15 | display: inline-block;
16 | color: rgba(0, 0, 0, .5);
17 | padding: 4px 0;
18 | text-decoration: none;
19 | margin-left: 20px;
20 | :hover {
21 | border-bottom: 1px solid rgba(0, 0, 0, .1);
22 | }
23 | }
24 | `
25 |
--------------------------------------------------------------------------------
/themes/default/history.js:
--------------------------------------------------------------------------------
1 | import { createBrowserHistory } from 'history'
2 |
3 | const history = typeof window !== 'undefined'
4 | ? createBrowserHistory()
5 | : {}
6 |
7 | export default history
8 |
--------------------------------------------------------------------------------
/themes/default/icons/close.js:
--------------------------------------------------------------------------------
1 | import React from 'react'
2 |
3 | export default function (props) {
4 | return (
5 |
6 |
7 |
8 | )
9 | }
10 |
--------------------------------------------------------------------------------
/themes/default/icons/external.js:
--------------------------------------------------------------------------------
1 | import React from 'react'
2 |
3 | export default function (props) {
4 | return (
5 |
6 |
7 |
8 | )
9 | }
10 |
--------------------------------------------------------------------------------
/themes/default/icons/hamburger.js:
--------------------------------------------------------------------------------
1 | import React from 'react'
2 |
3 | export default function (props) {
4 | return (
5 |
6 |
7 |
8 | )
9 | }
10 |
--------------------------------------------------------------------------------
/themes/default/index.js:
--------------------------------------------------------------------------------
1 | import React from 'react'
2 | import ReactDOM from 'react-dom'
3 | import { Router } from 'react-router-dom'
4 | import { hydrate } from 'emotion'
5 | import { registerLanguage as registerHighlight } from 'react-syntax-highlighter/light'
6 | import { registerLanguage as registerPrism } from 'react-syntax-highlighter/prism-light'
7 | import { languages } from '@codegen/loadSyntax' // eslint-disable-line
8 | import history from './history'
9 | import App from './application'
10 |
11 | const { _EMOTION_IDS_ } = window
12 | const isDev = process.env.NODE_ENV === 'development'
13 |
14 | // https://github.com/timberio/gitdocs/issues/114
15 | // const render = isDev ? ReactDOM.render : ReactDOM.hydrate
16 | const render = ReactDOM.render
17 |
18 | isDev && module.hot && module.hot.accept()
19 | _EMOTION_IDS_ && hydrate(_EMOTION_IDS_)
20 |
21 | // Ensure required languages are registered
22 | const renderers = {
23 | hljs: registerHighlight,
24 | prism: registerPrism,
25 | }
26 |
27 | if (window) {
28 | const register = renderers[process.env.PROPS.config.syntax.renderer]
29 | languages.forEach(lang => register(lang.name, lang.func))
30 | }
31 |
32 | render(
33 |
34 |
35 | ,
36 | document.getElementById('gitdocs-app'),
37 | )
38 |
--------------------------------------------------------------------------------
/themes/default/loading/index.js:
--------------------------------------------------------------------------------
1 | import React from 'react'
2 | import ContentLoader from 'react-content-loader'
3 | import { Wrapper } from './styles'
4 |
5 | // @TODO: Make this a little more responsive by passing in 400 for the height
6 | // on smaller screens
7 | export default function (props) {
8 | return (
9 |
10 |
18 |
19 |
20 |
21 |
22 |
23 |
24 |
25 |
26 |
27 |
28 |
29 |
30 |
31 |
39 |
40 |
41 |
42 |
43 |
44 |
45 |
46 |
47 |
48 |
49 |
50 |
51 |
52 |
60 |
61 |
62 |
63 |
64 |
65 |
66 |
67 |
68 |
69 |
70 |
71 |
79 |
80 |
81 |
82 |
83 |
84 |
85 |
86 |
87 |
88 |
89 |
90 |
91 |
92 |
100 |
101 |
102 |
103 |
104 |
105 |
106 |
107 |
108 |
109 |
110 |
111 |
119 |
120 |
121 |
122 |
123 |
124 |
125 |
126 |
127 |
128 |
129 |
130 |
131 |
132 |
133 | )
134 | }
135 |
--------------------------------------------------------------------------------
/themes/default/loading/styles.js:
--------------------------------------------------------------------------------
1 | import styled from 'react-emotion'
2 |
3 | export const Wrapper = styled('div')`
4 | height: 1000px;
5 | width: 100%;
6 | `
7 |
--------------------------------------------------------------------------------
/themes/default/logo/index.js:
--------------------------------------------------------------------------------
1 | import React from 'react'
2 | import { Link } from 'react-router-dom'
3 | import {
4 | Wrapper,
5 | CustomLogo,
6 | GeneratedLogo,
7 | } from './styles'
8 |
9 | export default function (props) {
10 | return (
11 |
12 |
13 | {props.logo
14 | ?
15 | : {props.title} }
16 |
17 |
18 | )
19 | }
20 |
--------------------------------------------------------------------------------
/themes/default/logo/styles.js:
--------------------------------------------------------------------------------
1 | import styled from 'react-emotion'
2 |
3 | export const Wrapper = styled('div')`
4 | :hover {
5 | opacity: 0.7;
6 | }
7 | `
8 |
9 | export const CustomLogo = styled('div')`
10 | img {
11 | display: block;
12 | height: 35px;
13 | }
14 | `
15 |
16 | export const GeneratedLogo = styled('div')`
17 | color: #6457DF;
18 | font-size: 1.6rem;
19 | font-weight: 700;
20 | text-decoration: none;
21 | `
22 |
--------------------------------------------------------------------------------
/themes/default/markdown/index.js:
--------------------------------------------------------------------------------
1 | import React from 'react'
2 | // import Markdown from 'react-markdown'
3 | import Markdown from 'markdown-to-jsx'
4 | import { Wrapper } from './styles'
5 | import Code from './overrides/Code'
6 | import Header from './overrides/Header'
7 | import Link from './overrides/Link'
8 |
9 | export default function (props) {
10 | const options = {
11 | overrides: {
12 | code: {
13 | component: Code,
14 | props: {
15 | renderer: props.renderer,
16 | lineNumbers: props.lineNumbers
17 | }
18 | },
19 | h1: {
20 | component: Header,
21 | props: { level: 1 },
22 | },
23 | h2: {
24 | component: Header,
25 | props: { level: 2 },
26 | },
27 | h3: {
28 | component: Header,
29 | props: { level: 3 },
30 | },
31 | h4: {
32 | component: Header,
33 | props: { level: 4 },
34 | },
35 | h5: {
36 | component: Header,
37 | props: { level: 5 },
38 | },
39 | h6: {
40 | component: Header,
41 | props: { level: 6 },
42 | },
43 | a: Link,
44 | }
45 | }
46 |
47 | return (
48 |
49 |
50 | {props.source}
51 |
52 |
53 | )
54 | }
55 |
--------------------------------------------------------------------------------
/themes/default/markdown/overrides/Code.js:
--------------------------------------------------------------------------------
1 | import React from 'react'
2 | import Highlight from 'react-syntax-highlighter/light'
3 | import Prism from 'react-syntax-highlighter/prism-light'
4 | import { theme, languages } from '@codegen/loadSyntax' // eslint-disable-line
5 |
6 | export default function (props) {
7 | const { children, renderer, className, lineNumbers } = props
8 |
9 | if (!className) return {children}
10 |
11 | let language = className.split('-')[1]
12 |
13 | if (language) {
14 | language = language // language name aliases
15 | .replace(/^js$/, 'javascript')
16 |
17 | const languageRegistered = languages
18 | .findIndex(({ name }) => name === language) > -1
19 |
20 | if (!languageRegistered && process.env.NODE_ENV === 'development') {
21 | console.warn(`You have ${language} syntax in your page, but didn't include it in your config file!`)
22 | }
23 | }
24 |
25 | const Syntax = renderer === 'prism'
26 | ? Prism
27 | : Highlight
28 |
29 | return (
30 |
37 | {children}
38 |
39 | )
40 | }
41 |
--------------------------------------------------------------------------------
/themes/default/markdown/overrides/Header.js:
--------------------------------------------------------------------------------
1 | import React from 'react'
2 | import styled from 'react-emotion'
3 |
4 | const Link = styled('a')`
5 | color: rgba(0,0,0,.7);
6 | margin: 1rem 0 0 0;
7 | text-decoration: none;
8 | display: block;
9 |
10 | &:hover {
11 | ::after {
12 | position: relative;
13 | left: 10px;
14 | content: "\u21b5";
15 | font-weight: bold;
16 | display: inline-block;
17 | }
18 | }
19 | `
20 |
21 | const style = {
22 | display: 'inline-block'
23 | }
24 |
25 | const levels = {
26 | 1: 'h1',
27 | 2: 'h2',
28 | 3: 'h3',
29 | 4: 'h4',
30 | 5: 'h5',
31 | 6: 'h6',
32 | }
33 |
34 | export default function (props) {
35 | const {
36 | level = 1,
37 | children,
38 | } = props
39 |
40 | // Turn headers into linkable IDs
41 | const text = children[0]
42 | const itemId = typeof text === 'string'
43 | ? text
44 | .toLowerCase()
45 | .split(' ')
46 | .join('-')
47 | : ''
48 |
49 | const element = React.createElement(levels[level], { style }, children)
50 |
51 | return level <= 2
52 | ? {element}
53 | : element
54 | }
55 |
--------------------------------------------------------------------------------
/themes/default/markdown/overrides/Link.js:
--------------------------------------------------------------------------------
1 | import React from 'react'
2 | import { Link } from 'react-router-dom'
3 |
4 | export default function (props) {
5 | const { href, children, ...rest } = props
6 | const isExternal = /^https?:\/\//.test(href)
7 |
8 | return isExternal
9 | ? {children}
10 | : {children}
11 | }
12 |
--------------------------------------------------------------------------------
/themes/default/markdown/styles.js:
--------------------------------------------------------------------------------
1 | import styled from 'react-emotion'
2 |
3 | export const Wrapper = styled('div')`
4 | word-wrap: break-word;
5 | color: #2f3138;
6 |
7 | a {
8 | text-decoration: none;
9 | color: #5944CC;
10 | }
11 |
12 | h1, h2, h3, h4 {
13 | font-weight: bold;
14 | text-decoration: none;
15 | margin: 0;
16 | color: #0D0A2B;
17 | }
18 |
19 | a:first-of-type {
20 | margin-top: 0;
21 | }
22 |
23 | p {
24 | line-height: 1.7rem;
25 | }
26 |
27 | ul {
28 | list-style: disc;
29 | padding-left: 2rem;
30 | }
31 |
32 | img {
33 | max-width: 100%;
34 | }
35 |
36 | blockquote {
37 | border-left: 3px solid #b6b3da;
38 | padding-left: 1rem;
39 | font-style: italic;
40 | }
41 |
42 | pre {
43 | overflow-x: scroll;
44 | background: #FAFAFD !important;
45 | border-radius: 4px;
46 | border-radius: 3px;
47 | line-height: 19px;
48 | padding: .25rem;
49 | }
50 |
51 | // We need this since react-syntax-highlighter adds a pre
52 | pre pre { margin: 0; }
53 |
54 | code {
55 | border-radius: 4px;
56 | padding: 0 .15rem;
57 | display: inline-block;
58 | word-break: break-all;
59 | background: #EEEAFE;
60 | color: #5742CA;
61 | }
62 |
63 | pre code {
64 | border: none;
65 | word-break: break-all;
66 | display: block;
67 | background: inherit;
68 | color: #485672;
69 | }
70 |
71 | table {
72 | display: block;
73 | width: 100%;
74 | overflow: auto;
75 | }
76 |
77 | table th {
78 | font-weight: bold;
79 | }
80 |
81 | table th,
82 | table td {
83 | padding: 6px 13px;
84 | border: 1px solid #ddd;
85 | }
86 |
87 | table tr {
88 | background-color: #fff;
89 | border-top: 1px solid #ccc;
90 | }
91 |
92 | table tr:nth-child(2n) {
93 | background-color: #f8f8f8;
94 | }
95 |
96 | svg {
97 | height: 18px;
98 | width: 18px;
99 | margin-right: .5rem;
100 | margin-bottom: 2px;
101 | }
102 |
103 | hr {
104 | border-bottom-color: #eee;
105 | height: .25em;
106 | padding: 0;
107 | margin: 24px 0;
108 | background-color: #e7e7e7;
109 | border: 0;
110 | }
111 |
112 | code.hljs.shell:before, code.hljs.bash:before {
113 | content: "$";
114 | margin-right: 5px;
115 | color: #b4b1d8;
116 | }
117 |
118 | .hljs {
119 | background: #F4F5F6;
120 | }
121 |
122 | .hljs-string {
123 | color: #955CCB;
124 | }
125 |
126 | .hljs-attr {
127 | color: #4078f2;
128 | }
129 |
130 | .syntax-shell {
131 | padding-left: 5px !important;
132 | }
133 |
134 | .syntax-shell .react-syntax-highlighter-line-number {
135 | visibility: hidden;
136 | position: absolute;
137 | height: 0;
138 | }
139 |
140 | .syntax-shell .react-syntax-highlighter-line-number::after {
141 | content: "$";
142 | visibility: visible;
143 | left: 0px;
144 | top: -4px;
145 | position: absolute;
146 | }
147 | `
148 |
--------------------------------------------------------------------------------
/themes/default/not-found/index.js:
--------------------------------------------------------------------------------
1 | import React from 'react'
2 |
3 | export default function () {
4 | return (
5 | Page Not Found!
6 | )
7 | }
8 |
--------------------------------------------------------------------------------
/themes/default/page/index.js:
--------------------------------------------------------------------------------
1 | import React, { Component } from 'react'
2 | import Helmet from 'react-helmet'
3 | import axios from 'axios'
4 | import Markdown from '../markdown'
5 | import Breadcrumbs from '../breadcrumbs'
6 | import Loading from '../loading'
7 | import TocPage from '../toc/page'
8 | import TocFolder from '../toc/folder'
9 | import { ConfigContext } from '../context'
10 | import { Wrapper, ContentWrapper, MarkdownWrapper } from './styles'
11 |
12 | const Content = ({ content, config, route }) => {
13 | const defaultContent = '##### _You don\'t have any content here yet!_'
14 |
15 | // Prepend route title to content if `prependTitles` is set to true in config
16 | let md = content
17 | if (config.prefix_titles) {
18 | md = `# ${route.title} \n ${content}`
19 | }
20 |
21 | return (
22 |
23 |
27 |
28 | )
29 | }
30 |
31 | export default class Page extends Component {
32 | static displayName = 'Page'
33 |
34 | constructor (props) {
35 | super(props)
36 | this.state = {
37 | loading: !props.route.content,
38 | content: props.route.content,
39 | }
40 | }
41 |
42 | async componentDidMount () {
43 | const { socketUrl } = this.props.pageData
44 |
45 | if (process.env.NODE_ENV === 'development') {
46 | this._socket = new WebSocket(socketUrl)
47 |
48 | this._socket.addEventListener('open', evt => {
49 | this._socket.send(this.props.route.input)
50 | this.setState({ loading: true })
51 | })
52 |
53 | this._socket.addEventListener('message', evt => {
54 | const { content } = JSON.parse(evt.data)
55 | this.setState({ content, loading: false })
56 | })
57 | } else if (!this.state.content) {
58 | try {
59 | const {
60 | data: { content },
61 | } = await axios.get('index.json')
62 | this.setState({ content, loading: false })
63 | } catch (err) {
64 | console.error(`Could not get page content: ${err}`)
65 | }
66 | }
67 | }
68 |
69 | componentWillUnmount () {
70 | if (this._socket) {
71 | this._socket.close()
72 | }
73 | }
74 |
75 | render () {
76 | const {
77 | route,
78 | pageData: { sticky },
79 | } = this.props
80 |
81 | const {
82 | loading,
83 | content,
84 | } = this.state
85 |
86 | return (
87 |
88 | {config =>
89 |
90 |
91 | {config.name !== route.title &&
92 | {route.title} }
93 |
94 |
95 | {Array.isArray(route.breadcrumbs) &&
96 | }
97 |
98 | {loading
99 | ?
100 | : (
101 |
102 |
103 |
108 |
109 | {route.toc.page &&
110 | }
111 |
112 |
113 | {route.toc.folder &&
114 | }
115 |
116 | )}
117 |
118 | }
119 |
120 | )
121 | }
122 | }
123 |
--------------------------------------------------------------------------------
/themes/default/page/styles.js:
--------------------------------------------------------------------------------
1 | import styled from 'react-emotion'
2 |
3 | export const Wrapper = styled('div')`
4 | padding: 30px 50px;
5 | box-sizing: border-box;
6 | position: relative;
7 |
8 | @media(max-width: 1200px) {
9 | flex-direction: column-reverse;
10 | nav { margin-left: 0 }
11 | padding: 15px 50px;
12 | }
13 |
14 | @media(max-width: 500px) {
15 | padding: 10px 20px;
16 | }
17 | `
18 |
19 | export const ContentWrapper = styled('div')`
20 | display: flex;
21 | flex-direction: row;
22 | flex-wrap: wrap-reverse;
23 | justify-content: flex-start;
24 | `
25 |
26 | export const MarkdownWrapper = styled('div')`
27 | flex: 1;
28 | max-width: 850px;
29 |
30 | @media(max-width: 1200px) {
31 | padding: 0 50px 0 0;
32 | }
33 |
34 | @media(max-width: 600px) {
35 | padding: 0;
36 | }
37 | `
38 |
--------------------------------------------------------------------------------
/themes/default/routes.js:
--------------------------------------------------------------------------------
1 | import React, { Component } from 'react'
2 | import PropTypes from 'prop-types'
3 | import { Switch, Route, Redirect } from 'react-router-dom'
4 |
5 | class Routes extends Component {
6 | route = ({ items = [], ...data }) => {
7 | const {
8 | pageData,
9 | componentPage: Page,
10 | } = this.props
11 |
12 | return [
13 | items.map(this.route),
14 | data.input && [
15 | (
21 |
25 | )}
26 | />,
27 | data.url !== '/' &&
28 | ,
35 | ],
36 | ]
37 | }
38 |
39 | render () {
40 | const {
41 | manifest,
42 | component404: NotFound,
43 | } = this.props
44 |
45 | return (
46 |
47 | {this.route(manifest)}
48 |
49 |
50 | )
51 | }
52 | }
53 |
54 | Routes.propTypes = {
55 | manifest: PropTypes.object.isRequired,
56 | componentPage: PropTypes.func.isRequired,
57 | component404: PropTypes.func.isRequired,
58 | pageData: PropTypes.object.isRequired,
59 | }
60 |
61 | export default Routes
62 |
--------------------------------------------------------------------------------
/themes/default/search/db.js:
--------------------------------------------------------------------------------
1 | import { Search as JsSearch } from 'js-search'
2 |
3 | // Create search instance for indexed doc search
4 | export function createDB ({ ref, indices, items }) {
5 | const db = new JsSearch('url')
6 | indices.forEach(i => {
7 | db.addIndex(i)
8 | })
9 | db.addDocuments(items)
10 | return db
11 | }
12 |
--------------------------------------------------------------------------------
/themes/default/search/index.js:
--------------------------------------------------------------------------------
1 | import React, { Component } from 'react'
2 | import enhanceClickOutside from 'react-click-outside'
3 | import { Link } from 'react-router-dom'
4 | import axios from 'axios'
5 | import Highlight from 'react-highlight-words'
6 | import { Search as SearchIcon, ChevronRight } from 'react-feather'
7 | import { createDB } from './db'
8 | import strip from './strip'
9 | import history from '../history'
10 | import { ellipsify } from '../utils'
11 | import { Wrapper, Input, Results, Result, Center } from './styles'
12 |
13 | const UP = 'ArrowUp'
14 | const DOWN = 'ArrowDown'
15 | const ENTER = 'Enter'
16 | const ESCAPE = 'Escape'
17 |
18 | class Search extends Component {
19 | constructor (props) {
20 | super(props)
21 |
22 | // Basic state for querying and loading
23 | this.state = {
24 | query: '',
25 | loading: false,
26 | selectedIndex: 0,
27 | results: [],
28 | }
29 |
30 | // Index docs for search results
31 | this.loadResults()
32 | }
33 |
34 | componentDidUpdate (prevProps, prevState) {
35 | if (this.state.selectedIndex !== prevState.selectedIndex) {
36 | this.ensureActiveItemVisible()
37 | }
38 | }
39 |
40 | ensureActiveItemVisible () {
41 | if (!this.activeItem) return false
42 |
43 | const distanceFromTop = this.activeItem.offsetTop
44 | const height = this.activeItem.offsetHeight
45 | const scrollTop = this.results.scrollTop
46 | const clientHeight = this.results.clientHeight
47 |
48 | if (distanceFromTop === 0) {
49 | return this.results.scrollTop = 0
50 | }
51 |
52 | if (distanceFromTop < scrollTop) {
53 | return this.results.scrollTop = distanceFromTop
54 | }
55 |
56 | if ((distanceFromTop + height) > (scrollTop + clientHeight)) {
57 | return this.results.scrollTop = distanceFromTop - height
58 | }
59 | }
60 |
61 | async loadResults () {
62 | // Initialize search instance and set indices
63 | const resp = await axios.get('/db.json')
64 | this.db = createDB({
65 | ref: 'url',
66 | indices: ['title', 'content'],
67 | items: resp.data,
68 | })
69 | }
70 |
71 | handleChange = e => {
72 | const { value } = e.target
73 | this.setState({
74 | query: value,
75 | loading: value.length !== 0,
76 | selectedIndex: 0,
77 | }, async () => {
78 | const results = await this.fetchResults(value)
79 | this.setState({ results, loading: false })
80 | this.results.scrollTop = 0
81 | })
82 | }
83 |
84 | handleKeyUp = e => {
85 | const { key } = e
86 |
87 | if (e.key === ESCAPE) {
88 | return this.clearSearch()
89 | }
90 |
91 | // Only listen for key up, down, and enter
92 | if (
93 | !key === UP &&
94 | !key === DOWN &&
95 | !key === ENTER
96 | ) return false
97 |
98 | // Get the selected index if it exists
99 | const { selectedIndex = 0, results } = this.state
100 |
101 | if (key === ENTER) {
102 | const selected = results[selectedIndex]
103 | if (selected) history.push(selected.url)
104 | this.clearSearch()
105 | }
106 |
107 | // Next selected index
108 | let nextIndex = selectedIndex
109 |
110 | if (key === UP) {
111 | if (selectedIndex === 0) {
112 | nextIndex = results.length - 1
113 | } else if (selectedIndex < 0) {
114 | nextIndex = results.length - 1
115 | } else {
116 | nextIndex = selectedIndex - 1
117 | }
118 | }
119 |
120 | if (key === DOWN) {
121 | if (selectedIndex === results.length - 1) {
122 | nextIndex = 0
123 | } else {
124 | nextIndex = selectedIndex + 1
125 | }
126 | }
127 |
128 | this.setState({ selectedIndex: nextIndex })
129 | }
130 |
131 | handleClickOutside () {
132 | this.clearSearch()
133 | }
134 |
135 | fetchResults (query) {
136 | return new Promise((resolve, reject) => {
137 | const results = this.db
138 | .search(query)
139 | .slice(0, 10)
140 | resolve(results)
141 | })
142 | }
143 |
144 | clearSearch = () => {
145 | this.setState({
146 | loading: false,
147 | query: '',
148 | results: [],
149 | selectedIndex: 0,
150 | })
151 | }
152 |
153 | renderBreadCrumb (result) {
154 | return result.breadcrumbs
155 | .slice(1, result.breadcrumbs.length)
156 | .map(({ title }, i) => (
157 |
158 | {i !== 0 && }
167 |
168 | {title}
169 |
170 | ))
171 | }
172 |
173 | renderResults () {
174 | const { query, loading, selectedIndex, results } = this.state
175 | if (!query.length) return null
176 |
177 | // Map over search results and create links
178 | const items = results.map((r, i) =>
179 | i === selectedIndex ? this.activeItem = ref : null}
183 | onClick={this.clearSearch}
184 | >
185 |
186 | {this.renderBreadCrumb(r)}
187 |
188 | 2 ? query.split(' ') : []}
191 | autoEscape
192 | textToHighlight={strip(ellipsify(r.content, 200))}
193 | />
194 |
195 | {r.url}
196 |
197 |
198 | )
199 |
200 | return (
201 | this.results = ref}>
202 | {items.length !== 0 && !loading && items}
203 | {items.length === 0 && !loading && No Results Found matching {`"${query}"`}... }
204 | {loading && Loading... }
205 |
206 | )
207 | }
208 |
209 | render () {
210 | return (
211 |
212 |
213 |
219 | {this.renderResults()}
220 |
221 | )
222 | }
223 | }
224 |
225 | export default enhanceClickOutside(Search)
226 |
--------------------------------------------------------------------------------
/themes/default/search/strip.js:
--------------------------------------------------------------------------------
1 | /* modified from https://github.com/stiang/remove-markdown */
2 | /* eslint-disable */
3 | module.exports = function (md, options) {
4 | options = options || {}
5 | options.listUnicodeChar = options.hasOwnProperty('listUnicodeChar') ? options.listUnicodeChar : false
6 | options.stripListLeaders = options.hasOwnProperty('stripListLeaders') ? options.stripListLeaders : true
7 | options.gfm = options.hasOwnProperty('gfm') ? options.gfm : true
8 |
9 | let output = md || ''
10 |
11 | // Remove horizontal rules (stripListHeaders conflict with this rule, which is why it has been moved to the top)
12 | output = output.replace(/^(-\s*?|\*\s*?|_\s*?){3,}\s*$/gm, '')
13 |
14 | try {
15 | if (options.stripListLeaders) {
16 | if (options.listUnicodeChar) { output = output.replace(/^([\s\t]*)([\*\-\+]|\d+\.)\s+/gm, `${options.listUnicodeChar} $1`) } else { output = output.replace(/^([\s\t]*)([\*\-\+]|\d+\.)\s+/gm, '$1') }
17 | }
18 | if (options.gfm) {
19 | output = output
20 | // Header
21 | .replace(/\n={2,}/g, '\n')
22 | // Fenced codeblocks
23 | .replace(/~{3}.*\n/g, '')
24 | // Strikethrough
25 | .replace(/~~/g, '')
26 | // Fenced codeblocks
27 | .replace(/`{3}.*\n/g, '')
28 | }
29 | output = output
30 | // Remove HTML tags
31 | .replace(/<[^>]*>/g, '')
32 | // Remove setext-style headers
33 | .replace(/^[=\-]{2,}\s*$/g, '')
34 | // Remove footnotes?
35 | .replace(/\[\^.+?\](\: .*?$)?/g, '')
36 | .replace(/\s{0,2}\[.*?\]: .*?$/g, '')
37 | // Remove images
38 | .replace(/\!\[.*?\][\[\(].*?[\]\)]/g, '')
39 | // Remove inline links
40 | .replace(/\[(.*?)\][\[\(].*?[\]\)]/g, '$1')
41 | // Remove blockquotes
42 | .replace(/^\s{0,3}>\s?/g, '')
43 | // Remove reference-style links?
44 | .replace(/^\s{1,2}\[(.*?)\]: (\S+)( ".*?")?\s*$/g, '')
45 | // Remove atx-style headers
46 | .replace(/^(\n)?\s{0,}#{1,6}\s+| {0,}(\n)?\s{0,}#{0,} {0,}(\n)?\s{0,}$/gm, '$1$2$3')
47 | // Remove emphasis (repeat the line to remove double emphasis)
48 | .replace(/([\*_]{1,3})(\S.*?\S{0,1})\1/g, '$2')
49 | .replace(/([\*_]{1,3})(\S.*?\S{0,1})\1/g, '$2')
50 | // Remove code blocks
51 | .replace(/(`{3,})(.*?)\1/gm, '$2')
52 | // Remove inline code
53 | .replace(/`(.+?)`/g, '$1')
54 | // Replace two or more newlines with exactly two? Not entirely sure this belongs here...
55 | .replace(/\n{2,}/g, '\n\n')
56 | } catch (e) {
57 | console.error(e)
58 | return md
59 | }
60 | return output
61 | }
62 |
--------------------------------------------------------------------------------
/themes/default/search/styles.js:
--------------------------------------------------------------------------------
1 | import styled from 'react-emotion'
2 |
3 | export const Wrapper = styled('div')`
4 | flex: 1;
5 | display: flex;
6 | position: relative;
7 | align-items: center;
8 | `
9 |
10 | export const Input = styled('input')`
11 | outline: none;
12 | font-size: .9rem;
13 | padding: 8px 15px;
14 | display: block;
15 | width: 200px;
16 | border: none;
17 | flex-grow: 1;
18 | ::placeholder {
19 | color: #ACB0B2;
20 | }
21 | `
22 |
23 | export const Results = styled('div')`
24 | position: absolute;
25 | width: 100%;
26 | top: 65px;
27 | height: 50vh;
28 | max-width: 700px;
29 | max-height: 700px;
30 | overflow: scroll;
31 | border: 1px solid rgba(0,0,0,.1);
32 | box-shadow: 0 0.5rem 1rem rgba(0,0,0,.175);
33 | background: #FFF;
34 | z-index: 99;
35 | `
36 |
37 | export const Result = styled('div')`
38 | padding: .5rem 1rem;
39 | background: ${props => props.selected ? '#f7f7fb' : '#FFF'};
40 | a { text-decoration: none }
41 | &:hover {
42 | h5 { color: #6457DC; text-decoration: underline; }
43 | }
44 | h5 {
45 | font-weight: bold;
46 | text-decoration: none;
47 | margin: 0;
48 | color: ${props => props.selected ? '#6457DC' : '#0d2b3e'};
49 | text-decoration: ${props => props.selected ? 'underline' : 'none'};
50 | }
51 | p {
52 | color: rgba(0,0,0,0.75);
53 | text-decoration: none;
54 | margin: 0;
55 | font-size: .9rem;
56 | }
57 | p .highlight {
58 | // text-decoration: underline;
59 | // text-decoration-color: #d1ccec
60 | border-bottom: 2px solid #b1a9da;
61 | display: inline-block;
62 | background: transparent;
63 | }
64 | .url {
65 | font-size: 12px;
66 | color: #5343a2;
67 | }
68 | `
69 |
70 | export const Center = styled('div')`
71 | position: absolute;
72 | top: 0;
73 | left: 0;
74 | width: 100%;
75 | height: 100%;
76 | display: flex;
77 | align-items: center;
78 | justify-content: center;
79 | `
80 |
--------------------------------------------------------------------------------
/themes/default/sidebar/index.js:
--------------------------------------------------------------------------------
1 | import React, { Component } from 'react'
2 | import PropTypes from 'prop-types'
3 | import { withRouter, NavLink } from 'react-router-dom'
4 | import styled from 'react-emotion'
5 | import { Reveal } from '@timberio/ui'
6 | import Logo from '../logo'
7 | import IconHamburger from '../icons/hamburger'
8 | import IconClose from '../icons/close'
9 | import IconExternal from '../icons/external'
10 | import { ConfigContext } from '../context'
11 | import {
12 | Wrapper,
13 | TopWrapper,
14 | MenuWrapper,
15 | Hamburger,
16 | Close,
17 | Nav,
18 | NavList,
19 | Callout,
20 | } from './styles'
21 |
22 | const Divider = styled('div')`
23 | border-bottom: 1px solid #dfe2e6;
24 | margin: .5rem 1rem .5rem 0;
25 | `
26 |
27 | const componentMap = {
28 | Divider
29 | }
30 |
31 | class Sidebar extends Component {
32 | static propTypes = {
33 | manifest: PropTypes.object,
34 | customLogo: PropTypes.string,
35 | }
36 |
37 | static defaultProps = {
38 | customLogo: null,
39 | manifest: {
40 | items: [],
41 | },
42 | }
43 |
44 | constructor () {
45 | super()
46 | this.state = {
47 | menuOpen: false,
48 | }
49 | }
50 |
51 | findActiveIndex = (items = []) => {
52 | const { pathname } = this.props.location
53 |
54 | return items.findIndex(item => {
55 | return item.url === pathname ||
56 | this.findActiveIndex(item.items) > -1
57 | })
58 | }
59 |
60 | renderTrigger = ({ title, url, input, component }) => {
61 | // @TODO: custom components
62 | if (component) {
63 | return React.createElement(componentMap[component])
64 | }
65 |
66 | if (/^https?:\/\//i.test(url)) {
67 | return (
68 |
73 | {title}
74 |
75 | )
76 | }
77 |
78 | // no index page, just children
79 | if (!input) {
80 | return {title}
81 | }
82 |
83 | return (
84 | this.setState({ menuOpen: false })}
88 | >
89 | {title}
90 |
91 | )
92 | }
93 |
94 | renderNavItems = (items, isFirst) => {
95 | return (
96 |
100 | {items
101 | .filter(i => !i.hidden)
102 | .map((item, i) => {
103 | return (
104 | this.renderTrigger(item)}
107 | >
108 | {item.items &&
109 | this.renderNavItems(item.items)}
110 |
111 | )
112 | })}
113 |
114 | )
115 | }
116 |
117 | render () {
118 | const {
119 | manifest,
120 | customLogo,
121 | } = this.props
122 |
123 | return (
124 |
125 | {config =>
126 |
127 |
128 |
133 |
134 | this.setState({ menuOpen: true })}
136 | role="presentation"
137 | >
138 |
139 |
140 |
141 |
142 |
143 | this.setState({ menuOpen: false })}
145 | role="presentation"
146 | >
147 |
148 |
149 |
150 |
151 | {this.renderNavItems(manifest.items, true)}
152 |
153 |
154 |
159 | Powered by GitDocs
160 |
161 |
162 | }
163 |
164 | )
165 | }
166 | }
167 |
168 | export default withRouter(Sidebar)
169 |
--------------------------------------------------------------------------------
/themes/default/sidebar/styles.js:
--------------------------------------------------------------------------------
1 | import styled, { css } from 'react-emotion'
2 | import { Accordion } from '@timberio/ui'
3 | import { filterProps } from '../utils'
4 |
5 | const _iconBase = css`
6 | width: 30px;
7 | height: 30px;
8 | fill: rgba(0, 0, 0, .5);
9 | cursor: pointer;
10 | :hover {
11 | opacity: 0.7;
12 | }
13 | `
14 |
15 | export const Wrapper = styled('div')`
16 | display: flex;
17 | flex-direction: column;
18 | height: 100vh;
19 | margin-left: auto;
20 | min-width: 270px;
21 | max-width: 270px;
22 | text-align: left;
23 | @media (max-width: 850px) {
24 | max-width: 100%;
25 | height: 70px;
26 | }
27 | svg {
28 | ${_iconBase};
29 | }
30 | `
31 |
32 | export const TopWrapper = styled('div')`
33 | flex-shrink: 0;
34 | display: flex;
35 | justify-content: flex-start;
36 | align-items: center;
37 | height: 70px;
38 | padding: 0 20px;
39 | border-bottom: 1px solid #E6E9EB;
40 | @media (max-width: 850px) {
41 | justify-content: space-between;
42 | }
43 | `
44 |
45 | export const MenuWrapper = styled('div')`
46 | flex: 1 1 auto;
47 | display: flex;
48 | flex-direction: column;
49 | overflow: scroll;
50 |
51 | @media (max-width: 850px) {
52 | background: #F5F7F9;
53 | box-shadow: -2px 0 3px rgba(0, 0, 0, .2);
54 | position: fixed;
55 | top: 0;
56 | bottom: 0;
57 | right: 0;
58 | z-index: 10;
59 | width: 100%;
60 | max-width: 300px;
61 | overflow: auto;
62 | opacity: ${props => props.open ? 1 : 0};
63 | transform: translateX(${props => props.open ? '0%' : '100%'});
64 | transition: transform .2s, opacity .2s;
65 | }
66 | `
67 |
68 | export const Close = styled('div')`
69 | flex-shrink: 0;
70 | display: none;
71 | align-items: center;
72 | justify-content: flex-end;
73 | height: 70px;
74 | padding: 0 20px;
75 | @media (max-width: 850px) {
76 | display: flex;
77 | }
78 | `
79 |
80 | export const Hamburger = styled('div')`
81 | display: none;
82 | @media (max-width: 850px) {
83 | display: block;
84 | }
85 | `
86 |
87 | export const Nav = styled('nav')`
88 | flex: 1 0 auto;
89 | padding: 20px 0 20px 15px;
90 | `
91 |
92 | export const NavList = styled(filterProps(Accordion, ['isFirst']))`
93 | padding-left: ${props => props.isFirst ? 0 : '20px'};
94 | a {
95 | display: flex;
96 | justify-content: space-between;
97 | align-items: center;
98 | cursor: pointer;
99 | color: ${props => props.isFirst ? '#0d2b3e' : '#4c555a'};
100 | font-size: ${props => props.isFirst ? '1rem' : '.9rem'};
101 | font-weight: ${props => props.isFirst ? 600 : 400};
102 | text-decoration: none;
103 | cursor: pointer;
104 | line-height: 24px;
105 | border-left: 3px solid transparent;
106 | padding: .1rem 1rem .1rem 0;
107 | // transition: ${props => props.isFirst ? 'none' : 'all 0.2s ease-in-out'};
108 | &:hover {
109 | opacity: 0.7;
110 | }
111 | &.active {
112 | font-weight: 600;
113 | ${props => props.isFirst && css`
114 | color: #6457DF;
115 | border-right: 3px solid #6457DF;
116 | `}
117 | :hover {
118 | opacity: 1;
119 | }
120 | }
121 | svg {
122 | fill: rgba(0, 0, 0, .5);
123 | width: 15px;
124 | height: 15px;
125 | margin-right: 15px;
126 | }
127 | }
128 | `
129 |
130 | export const Callout = styled('a')`
131 | flex-shrink: 0;
132 | color: rgba(0, 0, 0, .2);
133 | font-size: .8rem;
134 | font-weight: 900;
135 | padding: 20px 0;
136 | text-decoration: none;
137 | text-align: center;
138 | border-top: 1px solid #E6E9EB;
139 | :hover {
140 | color: rgba(0, 0, 0, .3);
141 | }
142 | `
143 |
--------------------------------------------------------------------------------
/themes/default/toc/folder.js:
--------------------------------------------------------------------------------
1 | import React from 'react'
2 | import PropTypes from 'prop-types'
3 | import { Wrapper, FolderItem } from './styles'
4 |
5 | const Toc = (props) => {
6 | return (
7 |
8 | {props.items.map(item => (
9 |
13 | {item.title}
14 | {item.description}
15 |
16 | ))}
17 |
18 | )
19 | }
20 |
21 | Toc.defaultProps = {
22 | items: [],
23 | }
24 |
25 | Toc.propTypes = {
26 | items: PropTypes.array,
27 | }
28 |
29 | export default Toc
30 |
--------------------------------------------------------------------------------
/themes/default/toc/page.js:
--------------------------------------------------------------------------------
1 | import React from 'react'
2 | import PropTypes from 'prop-types'
3 | import { PageItem } from './styles'
4 |
5 | const Toc = (props) => {
6 | // Don't show this if there aren't enough headers
7 | if (props.items.length < 2) return null
8 |
9 | // Create TOC hierarchy and link to headers
10 | const items = props.items.map(t => (
11 |
12 |
13 | {t.content}
14 |
15 |
16 | ))
17 |
18 | return (
19 |
20 |
21 |
Table of Contents
22 |
23 |
24 |
25 | )
26 | }
27 |
28 | Toc.defaultProps = {
29 | items: [],
30 | }
31 |
32 | Toc.propTypes = {
33 | items: PropTypes.array,
34 | }
35 |
36 | export default Toc
37 |
--------------------------------------------------------------------------------
/themes/default/toc/styles.js:
--------------------------------------------------------------------------------
1 | import styled, { css } from 'react-emotion'
2 | import { Link } from 'react-router-dom'
3 |
4 | export const Wrapper = styled('nav')`
5 | margin: 60px 0 0 5px;
6 | position: relative;
7 | `
8 |
9 | export const PageItem = styled('nav')`
10 | position: relative;
11 | margin: 0 0 30px 30px;
12 | width: 150px;
13 |
14 | ${props => props.sticky && css`
15 | @media (min-width: 1180px) {
16 | div {
17 | position: fixed;
18 | top: 30px;
19 | }
20 | }
21 | `}
22 |
23 | @media (max-width: 1180px) {
24 | width: 100%;
25 | }
26 |
27 | h5 {
28 | color: #848B8E;
29 | margin: 0 0 10px 0;
30 | opacity: .5;
31 | }
32 |
33 | ul {
34 | list-style: none;
35 | padding: 0;
36 | margin: 0;
37 | border-left: 1px solid #E6E9EB;
38 | padding-left: 20px;
39 | }
40 |
41 | li {
42 | font-size: 13px;
43 | line-height: 30px;
44 | display: block;
45 | }
46 |
47 | a {
48 | text-decoration: none;
49 | color: #626469;
50 | display: block;
51 | white-space: nowrap;
52 | overflow: hidden;
53 | text-overflow: ellipsis;
54 |
55 | &:hover {
56 | color: #5742C7;
57 | }
58 | }
59 | `
60 |
61 | export const FolderItem = styled(Link)`
62 | display: inline-block;
63 | width: 230px;
64 | margin-right: 50px;
65 | margin-bottom: 70px;
66 | vertical-align: top;
67 | text-decoration: none;
68 | color: #4c555a;
69 | position: relative;
70 | font-size: .9rem;
71 | transition: opacity .1s;
72 | b {
73 | display: block;
74 | font-weight: 600;
75 | font-size: 1rem;
76 | color: #0d2b3e;
77 | }
78 | &:hover {
79 | opacity: .5;
80 | }
81 | &:before {
82 | content: "";
83 | width: 50px;
84 | height: 3px;
85 | background: #6457DF;
86 | position: absolute;
87 | top: -10px;
88 | left: 0;
89 | opacity: .3;
90 | }
91 | `
92 |
--------------------------------------------------------------------------------
/themes/default/utils/index.js:
--------------------------------------------------------------------------------
1 | import React from 'react'
2 |
3 | export function ellipsify (text, limit) {
4 | if (!text) return ''
5 |
6 | if (text.length <= limit) {
7 | return text
8 | }
9 |
10 | return `${text.substring(0, limit)}...`
11 | }
12 |
13 | export function filterProps (element, whitelist) {
14 | return ({ children, ...props }) => {
15 | whitelist.forEach(i => delete props[i])
16 | return React.createElement(element, props, children)
17 | }
18 | }
19 |
--------------------------------------------------------------------------------
/themes/server.js:
--------------------------------------------------------------------------------
1 | import React from 'react'
2 | import { StaticRouter } from 'react-router-dom'
3 |
4 | export default function (props, route) {
5 | const { theme } = props.config
6 | const { default: App } = require(`./${theme}/application`)
7 |
8 | return (
9 |
13 |
14 |
15 | )
16 | }
17 |
--------------------------------------------------------------------------------