├── .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 |
104 |

Header Links →

105 |
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 |
31 |

Running Locally →

32 |
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 |
25 |

Syntax Highlighting →

26 |
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 |
30 |

Customizing Sidebar →

31 |
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 |
39 |

Getting Started →

40 |
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 |
21 |

Static Files →

22 |
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 |
23 |

Production Builds →

24 |
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 |
17 |

Using Sources →

18 |
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 |
34 |

Theming →

35 |
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 |
11 |

JSON Data →

12 |
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 |
85 |

Index Files →

86 |
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 | ![GitDocs Logo](docs/.static/logo-markdown.png) 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 | ![GitDocs Example](https://cl.ly/010X2F1Q413e/Screen%20Recording%202018-07-09%20at%2004.59%20PM.gif) 25 | 26 |
27 |


28 | Build Status 29 | Code Coverage 30 | Gitdocs Version 31 |

32 | MIT © Timber 33 |
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(' /foo0.5/bar1/bazdaily0.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 | 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 | ? Logo 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 | 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 |
      {items}
    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 | --------------------------------------------------------------------------------