First commit: this starts off on a pretty solid foot because we iterated on it a lot already.
This commit is contained in:
48
.eslintrc
Normal file
48
.eslintrc
Normal file
@ -0,0 +1,48 @@
|
|||||||
|
{
|
||||||
|
"parser": "babel-eslint",
|
||||||
|
"env": {
|
||||||
|
"browser": true,
|
||||||
|
"node": true
|
||||||
|
},
|
||||||
|
"plugins": [
|
||||||
|
"react",
|
||||||
|
"babel"
|
||||||
|
],
|
||||||
|
"globals": {
|
||||||
|
},
|
||||||
|
"extends": "eslint:recommended",
|
||||||
|
"rules": {
|
||||||
|
"comma-dangle": [2, "never"],
|
||||||
|
"jsx-quotes": [2, "prefer-single"],
|
||||||
|
"key-spacing": [0],
|
||||||
|
"max-len": [1, 360],
|
||||||
|
"no-bitwise": [2],
|
||||||
|
"no-console": [0],
|
||||||
|
"no-redeclare": [2],
|
||||||
|
"no-trailing-spaces": [2],
|
||||||
|
"no-undef": [2],
|
||||||
|
"no-underscore-dangle": [0],
|
||||||
|
"no-unused-vars": [2],
|
||||||
|
"no-use-before-define": [0],
|
||||||
|
"quotes": [2, "single"],
|
||||||
|
"react/display-name": [2, { "acceptTranspilerName": true }],
|
||||||
|
"react/jsx-no-bind": 2,
|
||||||
|
"react/jsx-no-duplicate-props": 2,
|
||||||
|
"react/jsx-no-undef": 2,
|
||||||
|
"react/jsx-uses-react": 2,
|
||||||
|
"react/react-in-jsx-scope": 2,
|
||||||
|
"react/no-unknown-property": [2],
|
||||||
|
"react/prop-types": 2,
|
||||||
|
"react/require-extension": [2, { "extensions": [".js", ".jsx"] }],
|
||||||
|
"react/self-closing-comp": [2],
|
||||||
|
"react/wrap-multilines": [2],
|
||||||
|
"semi": [2, "always"],
|
||||||
|
"space-after-keywords": [2, "always"],
|
||||||
|
"space-before-blocks": 2,
|
||||||
|
"space-before-function-paren": [2, "never"],
|
||||||
|
"space-before-keywords": [2, "always"],
|
||||||
|
"space-in-parens": [2, "never"],
|
||||||
|
"strict": [0],
|
||||||
|
"valid-typeof": 2
|
||||||
|
}
|
||||||
|
}
|
82
CONTRIBUTING.md
Normal file
82
CONTRIBUTING.md
Normal file
@ -0,0 +1,82 @@
|
|||||||
|
## Topics
|
||||||
|
|
||||||
|
Topics contain information that applies to web services in general (not specific to APIs).
|
||||||
|
|
||||||
|
## API endpoints
|
||||||
|
|
||||||
|
Each individual API should have its own markdown file in the /content directory. Use snake case for filenames.
|
||||||
|
|
||||||
|
Each API file should have:
|
||||||
|
|
||||||
|
- H2 naming the API (i.e. "Wobbles")
|
||||||
|
- description of the API
|
||||||
|
- H3 naming the object or resource that is retrieved/created/deleted by the API
|
||||||
|
- description of the method
|
||||||
|
- list of parameters
|
||||||
|
- example requests and responses
|
||||||
|
|
||||||
|
Make sure the API is also included in the nav by adding it to `content.js`.
|
||||||
|
|
||||||
|
### Description
|
||||||
|
|
||||||
|
A one- or two-sentence description explaining what the API does (not how to use it).
|
||||||
|
|
||||||
|
### Object
|
||||||
|
|
||||||
|
Each API should have a description of the primary resources returned and manipulated
|
||||||
|
using the API.
|
||||||
|
|
||||||
|
- H3 naming the object (i.e. "The wobble object")
|
||||||
|
- One sentence explaining what the object is.
|
||||||
|
- Two-column table to describe the object:
|
||||||
|
- property name / type / required
|
||||||
|
- property description
|
||||||
|
- If the object is severely nested, use a nested list instead of a table
|
||||||
|
|
||||||
|
### Methods
|
||||||
|
|
||||||
|
List all methods for interacting with the API.
|
||||||
|
|
||||||
|
Each method:
|
||||||
|
|
||||||
|
- H3 naming the method (i.e. "Retrieve a font")
|
||||||
|
- Endpoint (h4?)
|
||||||
|
- do not include base URL in endpoint
|
||||||
|
- endpoints should use [three backtick markdown format with syntax highlighting](https://help.github.com/articles/github-flavored-markdown/#syntax-highlighting) with `url` as the language
|
||||||
|
- A description of what the method does. (NOT how to use it.)
|
||||||
|
- (how do we define scopes?)
|
||||||
|
- If necessary, a description of accepted values/filetypes and limits/restrictions
|
||||||
|
- Two-column table to describe parameters
|
||||||
|
- parameter name
|
||||||
|
- parameter description and accepted values
|
||||||
|
|
||||||
|
### Examples
|
||||||
|
|
||||||
|
Each method should have H4 headers for examples:
|
||||||
|
|
||||||
|
- Example request
|
||||||
|
- Example request body (if applicable)
|
||||||
|
- Example response
|
||||||
|
|
||||||
|
There should be four examples under each header, one for each library. Use
|
||||||
|
[three backtick markdown format with syntax highlighting](https://help.github.com/articles/github-flavored-markdown/#syntax-highlighting):
|
||||||
|
|
||||||
|
## Style conventions
|
||||||
|
|
||||||
|
- Always JSON, never `JSON` or json.
|
||||||
|
- Do **not** include access tokens in example URLs.
|
||||||
|
- h2 and h3 will be included in side nav
|
||||||
|
- code blocks/h4/blockquotes will be pushed to the right
|
||||||
|
|
||||||
|
## Lingo
|
||||||
|
|
||||||
|
- the parts of a JSON object are called **properties**
|
||||||
|
- querystring parameters are called **parameters**
|
||||||
|
|
||||||
|
## Formatting JSON
|
||||||
|
|
||||||
|
We need to show JSON examples, but we want to make the documentation readable
|
||||||
|
on a wide range of monitors: so it needs to be somewhat narrow and compact.
|
||||||
|
Usually stringifying JSON with indentation of 2 spaces does the trick:
|
||||||
|
if that isn't enough, use [json-pretty-compact-cli](https://github.com/tmcw/json-pretty-compact-cli)
|
||||||
|
or another more tasteful formatter.
|
50
README.md
Normal file
50
README.md
Normal file
@ -0,0 +1,50 @@
|
|||||||
|
# Docbox
|
||||||
|
|
||||||
|
**REST API Documentation powered by Markdown**
|
||||||
|
|
||||||
|
Docbox is an open source version of Mapbox's REST API documentation system. It takes structured Markdown files and generates a friendly two-column layout with navigation, permalinks, and examples. The documentation source files that Docbox uses are friendly for documentation authors and free of presentational code: it's just Markdown.
|
||||||
|
|
||||||
|
_Docbox is a [Mapbox](http://mapbox.com/) community open source project. We built an awesome system for our REST API documentation and wanted to share it with you. Not a Mapbox product, so there's no guaranteed support and may have some rough edges._
|
||||||
|
|
||||||
|
Docbox is a JavaScript application written with React. The core magic is thanks to the [remark](http://remark.js.org/) Markdown parser, which enables the layout: after parsing a file into an [Abstract Syntax Tree](https://en.wikipedia.org/wiki/Abstract_syntax_tree), we can move examples to the right, prose to the left, and build the navigation system.
|
||||||
|
|
||||||
|
It also has a supercharged **test suite**. Our tests check for everything from broken links to invalid examples and structure problems: this way, the application is only concerned with output and you can proactively enforce consistency and correctness. We even extract JavaScript examples from documentation and test them with [eslint](http://eslint.org/)
|
||||||
|
|
||||||
|
When you're ready to ship, Docbox's `build` task minifies JavaScript and uses React's server rendering code to make documentation indexable for search engines and viewable without JavaScript.
|
||||||
|
|
||||||
|
## Writing Documentation
|
||||||
|
|
||||||
|
Documentation is written as Markdown files in the `content` directory, and is organized by the `src/content.js` file - that file requires each documentation page and puts them in order. This demo has a little bit of content - [content/example.md](content/example.md) and [content/introduction.md](content/introduction.md), so that there's an example to follow.
|
||||||
|
|
||||||
|
## Development
|
||||||
|
|
||||||
|
We care about the ease of writing documentation. Docbox comes with batteries included: after you `npm install` the project, you can run `npm start` and its development server, [budo](https://github.com/mattdesl/budo), will serve the website locally and update automatically.
|
||||||
|
|
||||||
|
To run the site locally:
|
||||||
|
|
||||||
|
1. Clone this repository
|
||||||
|
2. `git clone https://github.com/mapbox/docbox.git`
|
||||||
|
2. `npm install`
|
||||||
|
3. `npm start`
|
||||||
|
4. Open http://localhost:9966/
|
||||||
|
|
||||||
|
## Tests
|
||||||
|
|
||||||
|
Tests cover both the source code of Docbox as well as the content in the `content/` directory.
|
||||||
|
|
||||||
|
To run tests:
|
||||||
|
|
||||||
|
1. Clone this repository
|
||||||
|
2. `git clone https://github.com/mapbox/docbox.git`
|
||||||
|
2. `npm install`
|
||||||
|
3. `npm test`
|
||||||
|
|
||||||
|
|
||||||
|
## Deployment
|
||||||
|
|
||||||
|
The `npm run build` command builds a `bundle.js` file that contains all of the JavaScript code and content needed to show the site, and creates an `index.html` file that already contains the site content. Note that this _replaces_ the existing `index.html` file, so it's best to run this only when deploying the site and to undo changes to `index.html` if you want to keep working on content.
|
||||||
|
|
||||||
|
1. Clone this repository
|
||||||
|
2. `git clone https://github.com/mapbox/docbox.git`
|
||||||
|
2. `npm install`
|
||||||
|
3. `npm run build`
|
431
content/example.md
Normal file
431
content/example.md
Normal file
@ -0,0 +1,431 @@
|
|||||||
|
## Wobble
|
||||||
|
|
||||||
|
This is our high-quality wobbles API. You can use this API to request
|
||||||
|
and remove different wobbles at a low wibble price.
|
||||||
|
|
||||||
|
### List wobbles
|
||||||
|
|
||||||
|
Lists all wobbles for a particular account.
|
||||||
|
|
||||||
|
```endpoint
|
||||||
|
GET /wobbles/v1/{username} wobbles:read
|
||||||
|
```
|
||||||
|
|
||||||
|
#### Example request
|
||||||
|
|
||||||
|
```curl
|
||||||
|
$ curl https://wobble.biz/wobbles/v1/{username}
|
||||||
|
```
|
||||||
|
|
||||||
|
```bash
|
||||||
|
$ wbl wobbles list
|
||||||
|
```
|
||||||
|
|
||||||
|
```javascript
|
||||||
|
client.listWobbles(function(err, wobbles) {
|
||||||
|
console.log(wobbles);
|
||||||
|
});
|
||||||
|
```
|
||||||
|
|
||||||
|
```python
|
||||||
|
wobbles.list()
|
||||||
|
```
|
||||||
|
|
||||||
|
#### Example response
|
||||||
|
|
||||||
|
```json
|
||||||
|
[
|
||||||
|
{
|
||||||
|
"owner": "{username}",
|
||||||
|
"id": "{wobble_id}",
|
||||||
|
"created": "{timestamp}",
|
||||||
|
"modified": "{timestamp}"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"owner": "{username}",
|
||||||
|
"id": "{wobble_id}",
|
||||||
|
"created": "{timestamp}",
|
||||||
|
"modified": "{timestamp}"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
```
|
||||||
|
|
||||||
|
### Create wobble
|
||||||
|
|
||||||
|
Creates a new, empty wobble.
|
||||||
|
|
||||||
|
```endpoint
|
||||||
|
POST /wobbles/v1/{username} wobbles:write
|
||||||
|
```
|
||||||
|
|
||||||
|
#### Example request
|
||||||
|
|
||||||
|
```curl
|
||||||
|
curl -X POST https://wobble.biz/wobbles/v1/{username}
|
||||||
|
```
|
||||||
|
|
||||||
|
```bash
|
||||||
|
$ wbl wobbles create
|
||||||
|
```
|
||||||
|
|
||||||
|
```javascript
|
||||||
|
client.createWobble({
|
||||||
|
name: 'example',
|
||||||
|
description: 'An example wobble'
|
||||||
|
}, function(err, wobble) {
|
||||||
|
console.log(wobble);
|
||||||
|
});
|
||||||
|
```
|
||||||
|
|
||||||
|
```python
|
||||||
|
response = wobbles.create(
|
||||||
|
name='example', description='An example wobble')
|
||||||
|
```
|
||||||
|
|
||||||
|
#### Example request body
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"name": "foo",
|
||||||
|
"description": "bar"
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
Property | Description
|
||||||
|
---|---
|
||||||
|
`name` | (optional) the name of the wobble
|
||||||
|
`description` | (optional) a description of the wobble
|
||||||
|
|
||||||
|
#### Example response
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"owner": "{username}",
|
||||||
|
"id": "{wobble_id}",
|
||||||
|
"name": null,
|
||||||
|
"description": null,
|
||||||
|
"created": "{timestamp}",
|
||||||
|
"modified": "{timestamp}"
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### Retrieve a wobble
|
||||||
|
|
||||||
|
Returns a single wobble.
|
||||||
|
|
||||||
|
```endpoint
|
||||||
|
GET /wobbles/v1/{username}/{wobble_id} wobbles:read
|
||||||
|
```
|
||||||
|
|
||||||
|
Retrieve information about an existing wobble.
|
||||||
|
|
||||||
|
#### Example request
|
||||||
|
|
||||||
|
```curl
|
||||||
|
curl https://wobble.biz/wobbles/v1/{username}/{wobble_id}
|
||||||
|
```
|
||||||
|
|
||||||
|
```bash
|
||||||
|
$ wbl wobble read-wobble wobble-id
|
||||||
|
```
|
||||||
|
|
||||||
|
```python
|
||||||
|
attrs = wobbles.read_wobble(wobble_id).json()
|
||||||
|
```
|
||||||
|
|
||||||
|
```javascript
|
||||||
|
client.readWobble('wobble-id',
|
||||||
|
function(err, wobble) {
|
||||||
|
console.log(wobble);
|
||||||
|
});
|
||||||
|
```
|
||||||
|
|
||||||
|
#### Example response
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"owner": "{username}",
|
||||||
|
"id": "{wobble_id}",
|
||||||
|
"created": "{timestamp}",
|
||||||
|
"modified": "{timestamp}"
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### Update a wobble
|
||||||
|
|
||||||
|
Updates the properties of a particular wobble.
|
||||||
|
|
||||||
|
```endpoint
|
||||||
|
PATCH /wobbles/v1/{username}/{wobble_id} wobbles:write
|
||||||
|
```
|
||||||
|
|
||||||
|
#### Example request
|
||||||
|
|
||||||
|
```curl
|
||||||
|
curl --request PATCH https://wobble.biz/wobbles/v1/{username}/{wobble_id} \
|
||||||
|
-d @data.json
|
||||||
|
```
|
||||||
|
|
||||||
|
```python
|
||||||
|
resp = wobbles.update_wobble(
|
||||||
|
wobble_id,
|
||||||
|
name='updated example',
|
||||||
|
description='An updated example wobble'
|
||||||
|
).json()
|
||||||
|
```
|
||||||
|
|
||||||
|
```bash
|
||||||
|
$ wbl wobble update-wobble wobble-id
|
||||||
|
```
|
||||||
|
|
||||||
|
```javascript
|
||||||
|
var options = { name: 'foo' };
|
||||||
|
client.updateWobble('wobble-id', options, function(err, wobble) {
|
||||||
|
console.log(wobble);
|
||||||
|
});
|
||||||
|
```
|
||||||
|
|
||||||
|
#### Example request body
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"name": "foo",
|
||||||
|
"description": "bar"
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
Property | Description
|
||||||
|
---|---
|
||||||
|
`name` | (optional) the name of the wobble
|
||||||
|
`description` | (optional) a description of the wobble
|
||||||
|
|
||||||
|
#### Example response
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"owner": "{username}",
|
||||||
|
"id": "{wobble_id}",
|
||||||
|
"name": "foo",
|
||||||
|
"description": "bar",
|
||||||
|
"created": "{timestamp}",
|
||||||
|
"modified": "{timestamp}"
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### Delete a wobble
|
||||||
|
|
||||||
|
Deletes a wobble, including all wibbles it contains.
|
||||||
|
|
||||||
|
```endpoint
|
||||||
|
DELETE /wobbles/v1/{username}/{wobble_id} wobbles:write
|
||||||
|
```
|
||||||
|
|
||||||
|
#### Example request
|
||||||
|
|
||||||
|
```curl
|
||||||
|
curl -X DELETE https://wobble.biz/wobbles/v1/{username}/{wobble_id}
|
||||||
|
```
|
||||||
|
|
||||||
|
```bash
|
||||||
|
$ wbl wobble delete-wobble wobble-id
|
||||||
|
```
|
||||||
|
|
||||||
|
```python
|
||||||
|
resp = wobbles.delete_wobble(wobble_id)
|
||||||
|
```
|
||||||
|
|
||||||
|
```javascript
|
||||||
|
client.deleteWobble('wobble-id', function(err) {
|
||||||
|
if (!err) console.log('deleted!');
|
||||||
|
});
|
||||||
|
```
|
||||||
|
|
||||||
|
#### Example response
|
||||||
|
|
||||||
|
> HTTP 204
|
||||||
|
|
||||||
|
### List wibbles
|
||||||
|
|
||||||
|
List all the wibbles in a wobble. The response body will be a
|
||||||
|
WobbleCollection.
|
||||||
|
|
||||||
|
```endpoint
|
||||||
|
GET /wobbles/v1/{username}/{wobble_id}/wibbles wobbles:read
|
||||||
|
```
|
||||||
|
|
||||||
|
#### Example request
|
||||||
|
|
||||||
|
```curl
|
||||||
|
curl https://wobble.biz/wobbles/v1/{username}/{wobble_id}/wibbles
|
||||||
|
```
|
||||||
|
|
||||||
|
```bash
|
||||||
|
$ wbl wobble list-wibbles wobble-id
|
||||||
|
```
|
||||||
|
|
||||||
|
```python
|
||||||
|
collection = wobbles.list_wibbles(wobble_id).json()
|
||||||
|
```
|
||||||
|
|
||||||
|
```javascript
|
||||||
|
client.listWobbles('wobble-id', {}, function(err, collection) {
|
||||||
|
console.log(collection);
|
||||||
|
});
|
||||||
|
```
|
||||||
|
|
||||||
|
#### Example response
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"type": "Wobble",
|
||||||
|
"wibbles": [
|
||||||
|
{
|
||||||
|
"id": "{wibble_id}",
|
||||||
|
"type": "Wobble",
|
||||||
|
"properties": {
|
||||||
|
"prop0": "value0"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "{wibble_id}",
|
||||||
|
"type": "Wobble",
|
||||||
|
"properties": {
|
||||||
|
"prop0": "value0"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### Insert or update a wibble
|
||||||
|
|
||||||
|
Inserts or updates a wibble in a wobble. If there's already a wibble
|
||||||
|
with the given ID in the wobble, it will be replaced. If there isn't
|
||||||
|
a wibble with that ID, a new wibble is created.
|
||||||
|
|
||||||
|
```endpoint
|
||||||
|
PUT /wobbles/v1/{username}/{wobble_id}/wibbles/{wibble_id} wobbles:write
|
||||||
|
```
|
||||||
|
|
||||||
|
#### Example request
|
||||||
|
|
||||||
|
```curl
|
||||||
|
curl https://wobble.biz/wobbles/v1/{username}/{wobble_id}/wibbles/{wibble_id} \
|
||||||
|
-X PUT \
|
||||||
|
-d @file.geojson
|
||||||
|
```
|
||||||
|
|
||||||
|
```bash
|
||||||
|
$ wbl wobble put-wibble wobble-id wibble-id 'geojson-wibble'
|
||||||
|
```
|
||||||
|
|
||||||
|
```javascript
|
||||||
|
var wibble = {
|
||||||
|
"type": "Wobble",
|
||||||
|
"properties": { "name": "Null Island" }
|
||||||
|
};
|
||||||
|
client.insertWobble(wibble, 'wobble-id', function(err, wibble) {
|
||||||
|
console.log(wibble);
|
||||||
|
});
|
||||||
|
```
|
||||||
|
|
||||||
|
#### Example request body
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"id": "{wibble_id}",
|
||||||
|
"type": "Wobble",
|
||||||
|
"properties": {
|
||||||
|
"prop0": "value0"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
Property | Description
|
||||||
|
--- | ---
|
||||||
|
`id` | the id of an existing wibble in the wobble
|
||||||
|
|
||||||
|
#### Example response
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"id": "{wibble_id}",
|
||||||
|
"type": "Wobble",
|
||||||
|
"properties": {
|
||||||
|
"prop0": "value0"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### Retrieve a wibble
|
||||||
|
|
||||||
|
Retrieves a wibble in a wobble.
|
||||||
|
|
||||||
|
```endpoint
|
||||||
|
GET /wobbles/v1/{username}/{wobble_id}/wibbles/{wibble_id} wobbles:read
|
||||||
|
```
|
||||||
|
|
||||||
|
#### Example request
|
||||||
|
|
||||||
|
```curl
|
||||||
|
curl https://wobble.biz/wobbles/v1/{username}/{wobble_id}/wibbles/{wibble_id}
|
||||||
|
```
|
||||||
|
|
||||||
|
```bash
|
||||||
|
$ wbl wobble read-wibble wobble-id wibble-id
|
||||||
|
```
|
||||||
|
|
||||||
|
```javascript
|
||||||
|
client.readWobble('wibble-id', 'wobble-id',
|
||||||
|
function(err, wibble) {
|
||||||
|
console.log(wibble);
|
||||||
|
});
|
||||||
|
```
|
||||||
|
|
||||||
|
```python
|
||||||
|
wibble = wobbles.read_wibble(wobble_id, '2').json()
|
||||||
|
```
|
||||||
|
|
||||||
|
#### Example response
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"id": "{wibble_id}",
|
||||||
|
"type": "Wobble",
|
||||||
|
"properties": {
|
||||||
|
"prop0": "value0"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### Delete a wibble
|
||||||
|
|
||||||
|
Removes a wibble from a wobble.
|
||||||
|
|
||||||
|
```endpoint
|
||||||
|
DELETE /wobbles/v1/{username}/{wobble_id}/wibbles/{wibble_id} wobbles:write
|
||||||
|
```
|
||||||
|
|
||||||
|
#### Example request
|
||||||
|
|
||||||
|
```javascript
|
||||||
|
client.deleteWobble('wibble-id', 'wobble-id', function(err, wibble) {
|
||||||
|
if (!err) console.log('deleted!');
|
||||||
|
});
|
||||||
|
```
|
||||||
|
|
||||||
|
```curl
|
||||||
|
curl -X DELETE https://wobble.biz/wobbles/v1/{username}/{wobble_id}/wibbles/{wibble_id}
|
||||||
|
```
|
||||||
|
|
||||||
|
```python
|
||||||
|
resp = wobbles.delete_wibble(wobble_id, wibble_id)
|
||||||
|
```
|
||||||
|
|
||||||
|
```bash
|
||||||
|
$ wbl wobble delete-wibble wobble-id wibble-id
|
||||||
|
```
|
||||||
|
|
||||||
|
#### Example response
|
||||||
|
|
||||||
|
> HTTP 204
|
3
content/introduction.md
Normal file
3
content/introduction.md
Normal file
@ -0,0 +1,3 @@
|
|||||||
|
## Our API
|
||||||
|
|
||||||
|
Welcome to coolcorp biz! This is our API documentation.
|
3758
css/base.css
Normal file
3758
css/base.css
Normal file
File diff suppressed because it is too large
Load Diff
100
css/railscasts.css
Normal file
100
css/railscasts.css
Normal file
@ -0,0 +1,100 @@
|
|||||||
|
.hljs {
|
||||||
|
display: block;
|
||||||
|
overflow-x: auto;
|
||||||
|
padding: 0.5em;
|
||||||
|
background: #232323;
|
||||||
|
color: #e6e1dc;
|
||||||
|
}
|
||||||
|
|
||||||
|
.hljs-comment,
|
||||||
|
.hljs-quote {
|
||||||
|
color: #bc9458;
|
||||||
|
font-style: italic;
|
||||||
|
}
|
||||||
|
|
||||||
|
.hljs-keyword,
|
||||||
|
.hljs-selector-tag {
|
||||||
|
color: #c26230;
|
||||||
|
}
|
||||||
|
|
||||||
|
.hljs-string,
|
||||||
|
.hljs-number,
|
||||||
|
.hljs-regexp,
|
||||||
|
.hljs-variable,
|
||||||
|
.hljs-template-variable {
|
||||||
|
color: #a5c261;
|
||||||
|
}
|
||||||
|
|
||||||
|
.hljs-subst {
|
||||||
|
color: #519f50;
|
||||||
|
}
|
||||||
|
|
||||||
|
.hljs-tag,
|
||||||
|
.hljs-name {
|
||||||
|
color: #e8bf6a;
|
||||||
|
}
|
||||||
|
|
||||||
|
.hljs-type {
|
||||||
|
color: #da4939;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
.hljs-symbol,
|
||||||
|
.hljs-bullet,
|
||||||
|
.hljs-built_in,
|
||||||
|
.hljs-builtin-name,
|
||||||
|
.hljs-attr,
|
||||||
|
.hljs-link {
|
||||||
|
color: #6d9cbe;
|
||||||
|
}
|
||||||
|
|
||||||
|
.hljs-params {
|
||||||
|
color: #d0d0ff;
|
||||||
|
}
|
||||||
|
|
||||||
|
.hljs-attribute {
|
||||||
|
color: #cda869;
|
||||||
|
}
|
||||||
|
|
||||||
|
.hljs-meta {
|
||||||
|
color: #9b859d;
|
||||||
|
}
|
||||||
|
|
||||||
|
.hljs-title,
|
||||||
|
.hljs-section {
|
||||||
|
color: #ffc66d;
|
||||||
|
}
|
||||||
|
|
||||||
|
.hljs-addition {
|
||||||
|
background-color: #144212;
|
||||||
|
color: #e6e1dc;
|
||||||
|
display: inline-block;
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.hljs-deletion {
|
||||||
|
background-color: #600;
|
||||||
|
color: #e6e1dc;
|
||||||
|
display: inline-block;
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.hljs-selector-class {
|
||||||
|
color: #9b703f;
|
||||||
|
}
|
||||||
|
|
||||||
|
.hljs-selector-id {
|
||||||
|
color: #8b98ab;
|
||||||
|
}
|
||||||
|
|
||||||
|
.hljs-emphasis {
|
||||||
|
font-style: italic;
|
||||||
|
}
|
||||||
|
|
||||||
|
.hljs-strong {
|
||||||
|
font-weight: bold;
|
||||||
|
}
|
||||||
|
|
||||||
|
.hljs-link {
|
||||||
|
text-decoration: underline;
|
||||||
|
}
|
151
css/style.css
Normal file
151
css/style.css
Normal file
@ -0,0 +1,151 @@
|
|||||||
|
/* BASE OVERRIDES */
|
||||||
|
|
||||||
|
/* Add buffer for nicer anchor links */
|
||||||
|
.prose h2:first-child {
|
||||||
|
padding-top: 20px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.prose h3:first-child {
|
||||||
|
padding-top: 20px;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Header gets big */
|
||||||
|
@media only screen and (max-width:960px) {
|
||||||
|
.prose h2:first-child {
|
||||||
|
padding-top: 60px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.prose h3:first-child {
|
||||||
|
padding-top: 60px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Header gets bigger */
|
||||||
|
@media only screen and (max-width:640px) {
|
||||||
|
.prose h2:first-child {
|
||||||
|
padding-top: 100px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.prose h3:first-child {
|
||||||
|
padding-top: 100px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
/* tables are too giant */
|
||||||
|
.prose table,
|
||||||
|
.prose table code {
|
||||||
|
margin-bottom: 20px;
|
||||||
|
font-size: 12px;
|
||||||
|
line-height: 1.5;
|
||||||
|
}
|
||||||
|
|
||||||
|
.prose table th, .prose table td {
|
||||||
|
padding: 5px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.scroll-styled::-webkit-scrollbar {
|
||||||
|
width: 6px;
|
||||||
|
height: 6px;
|
||||||
|
background: transparent;
|
||||||
|
}
|
||||||
|
.scroll-styled::-webkit-scrollbar:hover {
|
||||||
|
background: transparent;
|
||||||
|
}
|
||||||
|
.scroll-styled::-webkit-scrollbar-track {
|
||||||
|
background:none;
|
||||||
|
}
|
||||||
|
.scroll-styled::-webkit-scrollbar-thumb {
|
||||||
|
background: rgba(0,0,0,.18);
|
||||||
|
width: 6px;
|
||||||
|
border:none;
|
||||||
|
border-radius: 3px;
|
||||||
|
}
|
||||||
|
.scroll-styled::-webkit-scrollbar-thumb:hover {
|
||||||
|
background: rgba(0,0,0,.25);
|
||||||
|
}
|
||||||
|
.scroll-styled::-webkit-scrollbar-track:hover {
|
||||||
|
background: transparent;
|
||||||
|
}
|
||||||
|
|
||||||
|
.dark.keyline-top,
|
||||||
|
.dark.keyline-bottom {
|
||||||
|
border-color: #313131;
|
||||||
|
}
|
||||||
|
|
||||||
|
html, body, #app, .container {
|
||||||
|
height: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
body pre {
|
||||||
|
padding: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* BASE ADDONS */
|
||||||
|
.fill-dark2 { background-color: #313131; }
|
||||||
|
.fill-dark2 .rounded-toggle input[type=radio]:checked + label,
|
||||||
|
.fill-dark2 .rounded-toggle .active { background-color: #313131; }
|
||||||
|
.space-top3 { margin-top: 30px;}
|
||||||
|
.space-top5 { margin-top: 50px;}
|
||||||
|
.line-height15 { line-height: 15px; }
|
||||||
|
.pad00y { padding-top: 2px; padding-bottom: 2px; }
|
||||||
|
.space-bottom00 { margin-bottom: 3px;}
|
||||||
|
|
||||||
|
.endpoint {
|
||||||
|
width: 100%;
|
||||||
|
display: flex;
|
||||||
|
}
|
||||||
|
|
||||||
|
.endpoint-method {
|
||||||
|
}
|
||||||
|
|
||||||
|
.endpoint-url {
|
||||||
|
flex-grow: 1;
|
||||||
|
word-wrap: break-word;
|
||||||
|
}
|
||||||
|
|
||||||
|
.endpoint-url a {
|
||||||
|
background-color: rgba(255, 255, 255, 0.1);
|
||||||
|
border-radius: 2px;
|
||||||
|
font-weight:bold;
|
||||||
|
padding: 2px;
|
||||||
|
color: #fff;
|
||||||
|
}
|
||||||
|
|
||||||
|
.endpoint-url a:hover {
|
||||||
|
background-color: rgba(255, 255, 255, 0.15);
|
||||||
|
}
|
||||||
|
|
||||||
|
.endpoint-token {
|
||||||
|
}
|
||||||
|
|
||||||
|
a.hljs-linked {
|
||||||
|
border-radius: 2px;
|
||||||
|
color: #a5c261;
|
||||||
|
background: #383C2F;
|
||||||
|
padding: 2px;
|
||||||
|
}
|
||||||
|
|
||||||
|
body .prose blockquote {
|
||||||
|
padding: 5px 10px;
|
||||||
|
background: #232323;
|
||||||
|
}
|
||||||
|
|
||||||
|
.preview {
|
||||||
|
background-image: -webkit-linear-gradient(#F1F075, #F1F075);
|
||||||
|
background-size: 10px 10px;
|
||||||
|
background-repeat: repeat-y;
|
||||||
|
}
|
||||||
|
|
||||||
|
.preview h2::after,
|
||||||
|
.preview h3::after {
|
||||||
|
content: 'PREVIEW';
|
||||||
|
background-color: #F1F075;
|
||||||
|
border-radius: 2px;
|
||||||
|
font-size: 9px;
|
||||||
|
font-weight: normal;
|
||||||
|
padding: 2px 5px;
|
||||||
|
color: rgba(0, 0, 0, 0.5);
|
||||||
|
margin-left: 10px;
|
||||||
|
vertical-align: middle;
|
||||||
|
}
|
17
index.html
Normal file
17
index.html
Normal file
@ -0,0 +1,17 @@
|
|||||||
|
<!DOCTYPE html>
|
||||||
|
<html>
|
||||||
|
<head>
|
||||||
|
<meta charset='utf-8' />
|
||||||
|
<meta http-equiv='X-UA-Compatible' content='IE=11' />
|
||||||
|
<title>Mapbox</title>
|
||||||
|
<meta name='viewport' content='initial-scale=1,maximum-scale=1,user-scalable=no' />
|
||||||
|
<link href='css/base.css' rel='stylesheet' />
|
||||||
|
<link rel='shortcut icon' href='/img/favicon.ico' type='image/x-icon' />
|
||||||
|
<link href='css/style.css' rel='stylesheet' />
|
||||||
|
<link href='css/railscasts.css' rel='stylesheet' />
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<div id='app'>APP</div>
|
||||||
|
<script src='bundle.js'></script>
|
||||||
|
</body>
|
||||||
|
</html>
|
64
package.json
Normal file
64
package.json
Normal file
@ -0,0 +1,64 @@
|
|||||||
|
{
|
||||||
|
"name": "docbox",
|
||||||
|
"version": "1.0.0",
|
||||||
|
"description": "an api documentation website",
|
||||||
|
"main": "index.js",
|
||||||
|
"scripts": {
|
||||||
|
"start": "budo src/index.js --serve=bundle.js --live",
|
||||||
|
"test": "npm run test-unit && eslint src && npm run prerender -- /dev/null && rm -rf lib",
|
||||||
|
"test-unit": "mocha --compilers js:babel-register test",
|
||||||
|
"prerender": "babel src --out-dir lib && node lib/render.js",
|
||||||
|
"build": "NODE_ENV=production browserify src/index.js | uglifyjs -c -m > bundle.js && npm run prerender -- index.html"
|
||||||
|
},
|
||||||
|
"browserify": {
|
||||||
|
"transform": [
|
||||||
|
"babelify",
|
||||||
|
"brfs"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"keywords": [
|
||||||
|
"api",
|
||||||
|
"documentation"
|
||||||
|
],
|
||||||
|
"author": "Tom MacWright",
|
||||||
|
"license": "ISC",
|
||||||
|
"dependencies": {
|
||||||
|
"babel-cli": "^6.4.0",
|
||||||
|
"babel-eslint": "^4.1.6",
|
||||||
|
"babel-polyfill": "^6.3.14",
|
||||||
|
"babel-preset-es2015": "^6.3.13",
|
||||||
|
"babel-preset-react": "^6.3.13",
|
||||||
|
"babel-preset-stage-0": "^6.3.13",
|
||||||
|
"babelify": "^7.2.0",
|
||||||
|
"brfs": "^1.4.2",
|
||||||
|
"browserify": "^13.0.0",
|
||||||
|
"cssnano": "^3.4.0",
|
||||||
|
"es6-promise": "^3.0.2",
|
||||||
|
"eslint": "^1.10.3",
|
||||||
|
"eslint-plugin-babel": "^3.0.0",
|
||||||
|
"eslint-plugin-react": "^3.14.0",
|
||||||
|
"github-slugger": "^1.0.1",
|
||||||
|
"isomorphic-fetch": "^2.2.0",
|
||||||
|
"lodash.debounce": "^4.0.3",
|
||||||
|
"minifyify": "^7.1.0",
|
||||||
|
"react": "^0.14.6",
|
||||||
|
"react-dom": "^0.14.6",
|
||||||
|
"react-pure-render": "^1.0.2",
|
||||||
|
"react-visibility-sensor": "^3.0.0",
|
||||||
|
"react-waypoint": "^1.2.0",
|
||||||
|
"remark": "^3.2.0",
|
||||||
|
"remark-highlight.js": "^2.0.0",
|
||||||
|
"remark-html": "^2.0.2",
|
||||||
|
"remark-slug": "^3.0.1",
|
||||||
|
"unist-util-select": "^1.3.0",
|
||||||
|
"unist-util-visit": "^1.0.0"
|
||||||
|
},
|
||||||
|
"devDependencies": {
|
||||||
|
"babel-register": "^6.3.13",
|
||||||
|
"expect": "^1.13.4",
|
||||||
|
"mocha": "^2.3.4",
|
||||||
|
"to-vfile": "^1.0.0",
|
||||||
|
"budo": "^7.1.0",
|
||||||
|
"uglifyjs": "^2.4.10"
|
||||||
|
}
|
||||||
|
}
|
171
src/components/app.js
Normal file
171
src/components/app.js
Normal file
@ -0,0 +1,171 @@
|
|||||||
|
import React from 'react';
|
||||||
|
import Navigation from './navigation';
|
||||||
|
import Content from './content';
|
||||||
|
import RoundedToggle from './rounded_toggle';
|
||||||
|
import PureRenderMixin from 'react-pure-render/mixin';
|
||||||
|
import GithubSlugger from 'github-slugger';
|
||||||
|
import debounce from 'lodash.debounce';
|
||||||
|
|
||||||
|
let slugger = new GithubSlugger();
|
||||||
|
let slug = title => { slugger.reset(); return slugger.slug(title); };
|
||||||
|
|
||||||
|
let languageOptions = ['curl', 'cli', 'python', 'javascript'];
|
||||||
|
|
||||||
|
let debouncedReplaceState = debounce(hash => {
|
||||||
|
window.history.replaceState('', '', hash);
|
||||||
|
}, 100);
|
||||||
|
|
||||||
|
var App = React.createClass({
|
||||||
|
mixins: [PureRenderMixin],
|
||||||
|
propTypes: {
|
||||||
|
content: React.PropTypes.string.isRequired,
|
||||||
|
ast: React.PropTypes.object.isRequired
|
||||||
|
},
|
||||||
|
getInitialState() {
|
||||||
|
var active = 'Introduction';
|
||||||
|
|
||||||
|
if (process.browser) {
|
||||||
|
let hash = window.location.hash.split('#').pop();
|
||||||
|
let mqls = {
|
||||||
|
'desktop': window.matchMedia('(min-width: 961px)'),
|
||||||
|
'tablet': window.matchMedia('(max-width: 960px)'),
|
||||||
|
'mobile': window.matchMedia('(max-width: 640px)')
|
||||||
|
};
|
||||||
|
Object.keys(mqls).forEach(key => {
|
||||||
|
mqls[key].addListener(this.mediaQueryChanged);
|
||||||
|
});
|
||||||
|
if (hash) {
|
||||||
|
let headingForHash = this.props.ast.children
|
||||||
|
.filter(child => child.type === 'heading')
|
||||||
|
.find(heading => heading.data.id === hash);
|
||||||
|
if (headingForHash) {
|
||||||
|
active = headingForHash.children[0].value;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return {
|
||||||
|
mqls: mqls,
|
||||||
|
queries: {},
|
||||||
|
language: 'curl',
|
||||||
|
activeSection: active,
|
||||||
|
showNav: false
|
||||||
|
};
|
||||||
|
} else {
|
||||||
|
return {
|
||||||
|
mqls: {
|
||||||
|
desktop: true
|
||||||
|
},
|
||||||
|
queries: {
|
||||||
|
desktop: true
|
||||||
|
},
|
||||||
|
language: 'curl',
|
||||||
|
activeSection: '',
|
||||||
|
showNav: false
|
||||||
|
};
|
||||||
|
}
|
||||||
|
},
|
||||||
|
toggleNav() {
|
||||||
|
this.setState({ showNav: !this.state.showNav });
|
||||||
|
},
|
||||||
|
componentDidMount() {
|
||||||
|
this.mediaQueryChanged();
|
||||||
|
},
|
||||||
|
mediaQueryChanged() {
|
||||||
|
var queries = {
|
||||||
|
mobile: this.state.mqls.mobile.matches,
|
||||||
|
tablet: this.state.mqls.tablet.matches,
|
||||||
|
desktop: this.state.mqls.desktop.matches
|
||||||
|
};
|
||||||
|
this.setState({ queries });
|
||||||
|
},
|
||||||
|
componentWillUnmount() {
|
||||||
|
Object.keys(this.state.mqls).forEach(key =>
|
||||||
|
this.state.mqls[key].removeListener(this.mediaQueryChanged));
|
||||||
|
},
|
||||||
|
onChangeLanguage(language) {
|
||||||
|
this.setState({ language });
|
||||||
|
},
|
||||||
|
onSpy(activeSection) {
|
||||||
|
this.setState({ activeSection });
|
||||||
|
},
|
||||||
|
componentDidUpdate(_, prevState) {
|
||||||
|
if (prevState.activeSection !== this.state.activeSection) {
|
||||||
|
// when the section changes, replace the hash
|
||||||
|
debouncedReplaceState(`#${slug(this.state.activeSection)}`);
|
||||||
|
} else if (prevState.language !== this.state.language) {
|
||||||
|
// when the language changes, use the hash to set scroll
|
||||||
|
window.location = window.location;
|
||||||
|
}
|
||||||
|
},
|
||||||
|
handleClick(activeSection) {
|
||||||
|
setTimeout(() => {
|
||||||
|
this.setState({ activeSection });
|
||||||
|
}, 10);
|
||||||
|
if (!this.state.queries.desktop) {
|
||||||
|
this.toggleNav();
|
||||||
|
}
|
||||||
|
},
|
||||||
|
render() {
|
||||||
|
let { ast } = this.props;
|
||||||
|
let { activeSection, queries, showNav } = this.state;
|
||||||
|
return (<div className='container unlimiter'>
|
||||||
|
|
||||||
|
{/* Content background */ }
|
||||||
|
{!queries.mobile && <div className={`fixed-top fixed-right ${queries.desktop && 'space-left16'}`}>
|
||||||
|
<div className='fill-dark col6 pin-right'></div>
|
||||||
|
</div>}
|
||||||
|
|
||||||
|
{/* Desktop nav */ }
|
||||||
|
{queries.desktop && <div className='space-top5 scroll-styled pad1 width16 sidebar fixed-left fill-light'>
|
||||||
|
<Navigation
|
||||||
|
handleClick={this.handleClick}
|
||||||
|
activeSection={activeSection}
|
||||||
|
ast={ast} />
|
||||||
|
</div>}
|
||||||
|
|
||||||
|
{/* Content */ }
|
||||||
|
<div className={`${queries.desktop && 'space-left16'}`}>
|
||||||
|
<Content
|
||||||
|
ast={ast}
|
||||||
|
queries={queries}
|
||||||
|
onSpy={this.onSpy}
|
||||||
|
language={this.state.language}/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Language toggle */ }
|
||||||
|
<div className={`fixed-top dark ${queries.desktop && 'space-left16'}`}>
|
||||||
|
<div className={`events fill-dark2 pad1 col6 pin-topright ${queries.mobile && 'space-top5 fixed-topright'}`}>
|
||||||
|
<RoundedToggle
|
||||||
|
options={languageOptions}
|
||||||
|
onChange={this.onChangeLanguage}
|
||||||
|
active={this.state.language} />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Header */ }
|
||||||
|
<div className={`fill-gray fixed-top ${queries.tablet ? 'pad1y pad2x col6' : 'pad0 width16'}`}>
|
||||||
|
<a href='/' className='active space-top1 space-left1 pin-topleft icon round fill-red dark pad0'></a>
|
||||||
|
<div className={`strong small pad0 ${queries.mobile && 'space-left3'} ${queries.tablet ? 'space-left2' : 'space-left4 line-height15' }`}>
|
||||||
|
{queries.desktop ? 'Example API Documentation' :
|
||||||
|
queries.mobile ? 'API Docs' : 'Example API Docs'}
|
||||||
|
</div>
|
||||||
|
{queries.tablet && <div>
|
||||||
|
<button
|
||||||
|
onClick={this.toggleNav}
|
||||||
|
className={`short quiet pin-topright micro button rcon ${showNav ? 'caret-up' : 'caret-down'} space-right1 space-top1`}>
|
||||||
|
{activeSection}
|
||||||
|
</button>
|
||||||
|
{showNav && <div
|
||||||
|
className='fixed-left fill-light pin-left col6 pad2 scroll-styled space-top5'>
|
||||||
|
<Navigation
|
||||||
|
handleClick={this.handleClick}
|
||||||
|
activeSection={activeSection}
|
||||||
|
ast={ast} />
|
||||||
|
</div>}
|
||||||
|
</div>}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
</div>);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
module.exports = App;
|
116
src/components/content.js
Normal file
116
src/components/content.js
Normal file
@ -0,0 +1,116 @@
|
|||||||
|
import React from 'react';
|
||||||
|
import Section from './section';
|
||||||
|
import PureRenderMixin from 'react-pure-render/mixin';
|
||||||
|
|
||||||
|
function highlightTokens(str) {
|
||||||
|
return str.replace(/{[\w_]+}/g,
|
||||||
|
(str) => '<span class="strong">' + str + '</span>')
|
||||||
|
.replace(
|
||||||
|
/{@2x}/g,
|
||||||
|
`<a title='Retina support: adding @2x to this URL will produce 2x scale images' href='#retina'>{@2x}</a>`);
|
||||||
|
}
|
||||||
|
|
||||||
|
function transformURL(node) {
|
||||||
|
let { value } = node;
|
||||||
|
let parts = value.split(/\s+/);
|
||||||
|
if (parts.length === 3) {
|
||||||
|
return {
|
||||||
|
type: 'html',
|
||||||
|
value: `<div class='endpoint'>
|
||||||
|
<div class='round-left pad0y pad1x fill-lighten0 code micro endpoint-method'>${parts[0]}</div>
|
||||||
|
<div class='fill-darken1 pad0 code micro endpoint-url'>${highlightTokens(parts[1])}</div>
|
||||||
|
<div class='endpoint-token contain fill-lighten0 pad0x round-topright'>
|
||||||
|
<span class='pad0 micro code'>${parts[2]}</span>
|
||||||
|
<a href='#access-tokens' class='center endpoint-scope space-top3 micro pad1x pin-top fill-lighten1 round-bottom'>
|
||||||
|
Token scope
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
</div>`
|
||||||
|
};
|
||||||
|
} else {
|
||||||
|
return {
|
||||||
|
type: 'html',
|
||||||
|
value: `<div class='endpoint'>
|
||||||
|
<div class='round-left pad0y pad1x fill-lighten0 code small endpoint-method'>${parts[0]}</div>
|
||||||
|
<div class='fill-darken1 pad0 code small endpoint-url'>${highlightTokens(parts[1])}</div>
|
||||||
|
</div>`
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function chunkifyAST(ast, language) {
|
||||||
|
var preview = false;
|
||||||
|
return ast.children.reduce((chunks, node) => {
|
||||||
|
if (node.type === 'heading' && node.depth === 1) {
|
||||||
|
return chunks;
|
||||||
|
} else if (node.type === 'heading' && node.depth < 4) {
|
||||||
|
chunks.push([node]);
|
||||||
|
} else {
|
||||||
|
chunks[chunks.length - 1].push(node);
|
||||||
|
}
|
||||||
|
return chunks;
|
||||||
|
}, [[]]).filter(chunk => chunk.length)
|
||||||
|
.map(chunk => {
|
||||||
|
var left = [], right = [], title;
|
||||||
|
if (language === 'cli') {
|
||||||
|
language = 'bash';
|
||||||
|
}
|
||||||
|
if (chunk[0].depth < 3) {
|
||||||
|
preview = false;
|
||||||
|
}
|
||||||
|
chunk.forEach(node => {
|
||||||
|
if (node.type === 'code') {
|
||||||
|
if (node.lang === 'json' || node.lang === 'http' || node.lang === 'html') {
|
||||||
|
right.push(node);
|
||||||
|
} else if (node.lang === language) {
|
||||||
|
if (language === 'curl') {
|
||||||
|
right.push({ ...node, lang: 'bash' });
|
||||||
|
} else {
|
||||||
|
right.push(node);
|
||||||
|
}
|
||||||
|
} else if (node.lang === 'endpoint') {
|
||||||
|
right.push(transformURL(node));
|
||||||
|
}
|
||||||
|
} else if (node.type === 'heading' && node.depth >= 4) {
|
||||||
|
right.push(node);
|
||||||
|
} else if (node.type === 'blockquote') {
|
||||||
|
right.push(node);
|
||||||
|
} else if (node.type === 'heading' && node.depth < 4 && !title) {
|
||||||
|
title = node.children[0].value;
|
||||||
|
left.push(node);
|
||||||
|
} else if (node.type === 'html') {
|
||||||
|
if (node.value.indexOf('<!--') === 0) {
|
||||||
|
var content = node.value
|
||||||
|
.replace(/^<!--/, '')
|
||||||
|
.replace(/-->$/, '')
|
||||||
|
.trim();
|
||||||
|
if (content === 'preview') {
|
||||||
|
preview = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
left.push(node);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
return { left, right, title, preview };
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
var Content = React.createClass({
|
||||||
|
mixins: [PureRenderMixin],
|
||||||
|
propTypes: {
|
||||||
|
ast: React.PropTypes.object.isRequired,
|
||||||
|
language: React.PropTypes.string.isRequired,
|
||||||
|
onSpy: React.PropTypes.func.isRequired
|
||||||
|
},
|
||||||
|
render() {
|
||||||
|
return (<div className='clearfix'>
|
||||||
|
{chunkifyAST(this.props.ast, this.props.language).map((chunk, i) => <Section
|
||||||
|
onSpy={this.props.onSpy}
|
||||||
|
chunk={chunk}
|
||||||
|
key={i} />)}
|
||||||
|
</div>);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
module.exports = Content;
|
94
src/components/navigation.js
Normal file
94
src/components/navigation.js
Normal file
@ -0,0 +1,94 @@
|
|||||||
|
import React from 'react';
|
||||||
|
import PureRenderMixin from 'react-pure-render/mixin';
|
||||||
|
import NavigationItem from './navigation_item';
|
||||||
|
|
||||||
|
function getAllInSectionFromChild(headings, idx) {
|
||||||
|
for (var i = idx; i > 0; i--) {
|
||||||
|
if (headings[i].depth === 2) {
|
||||||
|
return getAllInSection(headings, i);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function getAllInSection(headings, idx) {
|
||||||
|
var activeHeadings = [];
|
||||||
|
for (var i = idx + 1; i < headings.length; i++) {
|
||||||
|
if (headings[i].depth === 3) {
|
||||||
|
activeHeadings.push(headings[i].children[0].value);
|
||||||
|
} else if (headings[i].depth === 2) {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return activeHeadings;
|
||||||
|
}
|
||||||
|
|
||||||
|
var Navigation = React.createClass({
|
||||||
|
mixins: [PureRenderMixin],
|
||||||
|
propTypes: {
|
||||||
|
ast: React.PropTypes.object.isRequired,
|
||||||
|
activeSection: React.PropTypes.string,
|
||||||
|
handleClick: React.PropTypes.func.isRequired
|
||||||
|
},
|
||||||
|
render() {
|
||||||
|
var activeHeadings = [];
|
||||||
|
let headings = this.props.ast.children
|
||||||
|
.filter(child => child.type === 'heading');
|
||||||
|
|
||||||
|
if (this.props.activeSection) {
|
||||||
|
|
||||||
|
let activeHeadingIdx = headings.findIndex(heading =>
|
||||||
|
heading.children[0].value === this.props.activeSection);
|
||||||
|
let activeHeading = headings[activeHeadingIdx];
|
||||||
|
|
||||||
|
if (activeHeading.depth === 3) {
|
||||||
|
activeHeadings = [this.props.activeSection]
|
||||||
|
.concat(getAllInSectionFromChild(headings, activeHeadingIdx));
|
||||||
|
}
|
||||||
|
|
||||||
|
// this could potentially have children, try to find them
|
||||||
|
if (activeHeading.depth === 2) {
|
||||||
|
activeHeadings = [this.props.activeSection]
|
||||||
|
.concat(getAllInSection(headings, activeHeadingIdx));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
activeHeadings = activeHeadings.reduce((memo, heading) => {
|
||||||
|
memo[heading] = true;
|
||||||
|
return memo;
|
||||||
|
}, {});
|
||||||
|
|
||||||
|
return (<div className='pad0x small'>
|
||||||
|
{headings
|
||||||
|
.map((child, i) => {
|
||||||
|
let sectionName = child.children[0].value;
|
||||||
|
var active = sectionName === this.props.activeSection;
|
||||||
|
if (child.depth === 1) {
|
||||||
|
return (<div key={i}
|
||||||
|
onClick={this.handleClick}
|
||||||
|
className='small pad0x strong space-top1'>{sectionName}</div>);
|
||||||
|
} else if (child.depth === 2) {
|
||||||
|
return (<NavigationItem
|
||||||
|
key={i}
|
||||||
|
href={`#${child.data.id}`}
|
||||||
|
handleClick={this.props.handleClick}
|
||||||
|
active={active}
|
||||||
|
sectionName={sectionName} />);
|
||||||
|
} else if (child.depth === 3) {
|
||||||
|
if (activeHeadings.hasOwnProperty(sectionName)) {
|
||||||
|
return (<div
|
||||||
|
key={i}
|
||||||
|
className='space-left1'>
|
||||||
|
<NavigationItem
|
||||||
|
href={`#${child.data.id}`}
|
||||||
|
handleClick={this.props.handleClick}
|
||||||
|
active={active}
|
||||||
|
sectionName={sectionName} />
|
||||||
|
</div>);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})}
|
||||||
|
</div>);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
module.exports = Navigation;
|
26
src/components/navigation_item.js
Normal file
26
src/components/navigation_item.js
Normal file
@ -0,0 +1,26 @@
|
|||||||
|
import React from 'react';
|
||||||
|
import PureRenderMixin from 'react-pure-render/mixin';
|
||||||
|
|
||||||
|
var NavigationItem = React.createClass({
|
||||||
|
mixins: [PureRenderMixin],
|
||||||
|
propTypes: {
|
||||||
|
sectionName: React.PropTypes.string.isRequired,
|
||||||
|
active: React.PropTypes.bool.isRequired,
|
||||||
|
handleClick: React.PropTypes.func.isRequired,
|
||||||
|
href: React.PropTypes.string.isRequired
|
||||||
|
},
|
||||||
|
onClick() {
|
||||||
|
this.props.handleClick(this.props.sectionName);
|
||||||
|
},
|
||||||
|
render() {
|
||||||
|
var {sectionName, href, active} = this.props;
|
||||||
|
return (<a
|
||||||
|
href={href}
|
||||||
|
onClick={this.onClick}
|
||||||
|
className={`line-height15 pad0x pad00y block ${active ? 'fill-darken0 quiet active round' : ''}`}>
|
||||||
|
{sectionName}
|
||||||
|
</a>);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
module.exports = NavigationItem;
|
42
src/components/rounded_toggle.js
Normal file
42
src/components/rounded_toggle.js
Normal file
@ -0,0 +1,42 @@
|
|||||||
|
import React from 'react';
|
||||||
|
import PureRenderMixin from 'react-pure-render/mixin';
|
||||||
|
|
||||||
|
var RoundedToggle = React.createClass({
|
||||||
|
mixins: [PureRenderMixin],
|
||||||
|
propTypes: {
|
||||||
|
options: React.PropTypes.array.isRequired,
|
||||||
|
active: React.PropTypes.string.isRequired,
|
||||||
|
onChange: React.PropTypes.func.isRequired
|
||||||
|
},
|
||||||
|
render() {
|
||||||
|
let { options, active } = this.props;
|
||||||
|
return (<div className='rounded-toggle inline short'>
|
||||||
|
{options.map(option =>
|
||||||
|
<RoundedToggleOption
|
||||||
|
key={option}
|
||||||
|
option={option}
|
||||||
|
onClick={this.props.onChange}
|
||||||
|
className={'strong ' + (option === active ? 'active': '')} />)}
|
||||||
|
</div>);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
var RoundedToggleOption = React.createClass({
|
||||||
|
mixins: [PureRenderMixin],
|
||||||
|
propTypes: {
|
||||||
|
option: React.PropTypes.string.isRequired,
|
||||||
|
className: React.PropTypes.string.isRequired,
|
||||||
|
onClick: React.PropTypes.func.isRequired
|
||||||
|
},
|
||||||
|
onClick() {
|
||||||
|
this.props.onClick(this.props.option);
|
||||||
|
},
|
||||||
|
render() {
|
||||||
|
let { className, option } = this.props;
|
||||||
|
return (<a
|
||||||
|
onClick={this.onClick}
|
||||||
|
className={className}>{option}</a>);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
module.exports = RoundedToggle;
|
56
src/components/section.js
Normal file
56
src/components/section.js
Normal file
@ -0,0 +1,56 @@
|
|||||||
|
import React from 'react';
|
||||||
|
import remark from 'remark';
|
||||||
|
import remarkHTML from 'remark-html';
|
||||||
|
import remarkHighlight from 'remark-highlight.js';
|
||||||
|
import Waypoint from 'react-waypoint';
|
||||||
|
import PureRenderMixin from 'react-pure-render/mixin';
|
||||||
|
|
||||||
|
function renderHighlighted(nodes) {
|
||||||
|
return {
|
||||||
|
__html: remark()
|
||||||
|
.use(remarkHTML)
|
||||||
|
.stringify(remark().use(remarkHighlight).run({
|
||||||
|
type: 'root',
|
||||||
|
children: nodes
|
||||||
|
}))
|
||||||
|
.replace(
|
||||||
|
/<span class="hljs-string">"{timestamp}"<\/span>/g,
|
||||||
|
`<span class="hljs-string">"</span><a class='hljs-linked' href='#dates'>{timestamp}</a><span class="hljs-string">"</span>`)
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
var Section = React.createClass({
|
||||||
|
mixins: [PureRenderMixin],
|
||||||
|
propTypes: {
|
||||||
|
chunk: React.PropTypes.object.isRequired,
|
||||||
|
onSpy: React.PropTypes.func.isRequired
|
||||||
|
},
|
||||||
|
waypointEnterFromAbove(e, direction) {
|
||||||
|
if (direction === 'above') {
|
||||||
|
this.props.onSpy(this.props.chunk.title);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
waypointEnterFromBelow(e, direction) {
|
||||||
|
if (direction === 'below') {
|
||||||
|
this.props.onSpy(this.props.chunk.title);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
render() {
|
||||||
|
let { chunk } = this.props;
|
||||||
|
let { left, right, preview } = chunk;
|
||||||
|
return (<div className={`contain clearfix ${preview ? 'preview' : ''}`}>
|
||||||
|
<Waypoint onEnter={this.waypointEnterFromBelow} />
|
||||||
|
<div
|
||||||
|
className='col6 pad2x prose clip'
|
||||||
|
dangerouslySetInnerHTML={renderHighlighted(left)} />
|
||||||
|
{(right.length > 0) && <div
|
||||||
|
className='col6 pad2 prose dark space-top5 clip keyline-top fill-dark'
|
||||||
|
dangerouslySetInnerHTML={renderHighlighted(right)} />}
|
||||||
|
<div className='pin-bottom'>
|
||||||
|
<Waypoint onEnter={this.waypointEnterFromAbove} />
|
||||||
|
</div>
|
||||||
|
</div>);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
module.exports = Section;
|
7
src/content.js
Normal file
7
src/content.js
Normal file
@ -0,0 +1,7 @@
|
|||||||
|
var fs = require('fs');
|
||||||
|
|
||||||
|
module.exports =
|
||||||
|
'# Introduction\n' +
|
||||||
|
fs.readFileSync('./content/introduction.md', 'utf8') + '\n' +
|
||||||
|
'# Example\n' +
|
||||||
|
fs.readFileSync('./content/example.md', 'utf8') + '\n';
|
13
src/index.js
Normal file
13
src/index.js
Normal file
@ -0,0 +1,13 @@
|
|||||||
|
import 'babel-polyfill';
|
||||||
|
import React from 'react';
|
||||||
|
import ReactDOM from 'react-dom';
|
||||||
|
import App from './components/app';
|
||||||
|
import remark from 'remark';
|
||||||
|
import slug from 'remark-slug';
|
||||||
|
import content from './content';
|
||||||
|
|
||||||
|
var ast = remark().use(slug).run(remark().parse(content));
|
||||||
|
|
||||||
|
ReactDOM.render(
|
||||||
|
<App ast={ast} content={content} />,
|
||||||
|
document.getElementById('app'));
|
18
src/render.js
Normal file
18
src/render.js
Normal file
@ -0,0 +1,18 @@
|
|||||||
|
import React from 'react';
|
||||||
|
import ReactDOMServer from 'react-dom/server';
|
||||||
|
import App from './components/app';
|
||||||
|
import remark from 'remark';
|
||||||
|
import slug from 'remark-slug';
|
||||||
|
import content from './content';
|
||||||
|
import fs from 'fs';
|
||||||
|
|
||||||
|
var ast = remark().use(slug).run(remark().parse(content));
|
||||||
|
|
||||||
|
var template = fs.readFileSync('./index.html', 'utf8');
|
||||||
|
|
||||||
|
var target = process.argv[2];
|
||||||
|
|
||||||
|
fs.writeFileSync(target,
|
||||||
|
template.replace('APP',
|
||||||
|
ReactDOMServer.renderToString(
|
||||||
|
<App ast={ast} content={content} />)));
|
121
test/content.js
Normal file
121
test/content.js
Normal file
@ -0,0 +1,121 @@
|
|||||||
|
/* global it describe */
|
||||||
|
var remark = require('remark');
|
||||||
|
var expect = require('expect');
|
||||||
|
var visit = require('unist-util-visit');
|
||||||
|
var select = require('unist-util-select');
|
||||||
|
var fs = require('fs');
|
||||||
|
var GithubSlugger = require('github-slugger');
|
||||||
|
var { linter } = require('eslint');
|
||||||
|
var allPages = require('../src/content');
|
||||||
|
|
||||||
|
var slugger = new GithubSlugger();
|
||||||
|
var actionVerbs = /^(List|Retrieve|Remove|Search|Create|Delete)/;
|
||||||
|
|
||||||
|
let isSectionTitle = (title) => title.match(actionVerbs);
|
||||||
|
let getSectionTitle = chunk => chunk[0].children[0].value;
|
||||||
|
|
||||||
|
function extractSections(ast) {
|
||||||
|
return ast.children.reduce((chunks, node) => {
|
||||||
|
if (node.type === 'heading' && node.depth === 1) {
|
||||||
|
return chunks;
|
||||||
|
} else if (node.type === 'heading' && node.depth === 3) {
|
||||||
|
chunks.push([node]);
|
||||||
|
} else {
|
||||||
|
chunks[chunks.length - 1].push(node);
|
||||||
|
}
|
||||||
|
return chunks;
|
||||||
|
}, [[]])
|
||||||
|
.filter(chunk => chunk.length)
|
||||||
|
.filter(chunk => {
|
||||||
|
return isSectionTitle(getSectionTitle(chunk));
|
||||||
|
})
|
||||||
|
.map(chunk => ({ type: 'root', children: chunk }));
|
||||||
|
}
|
||||||
|
|
||||||
|
var slugs = {};
|
||||||
|
describe('global rules', () => {
|
||||||
|
var ast = remark.parse(allPages);
|
||||||
|
var seen = {};
|
||||||
|
/**
|
||||||
|
* Check that titles are unique. This is to ensure that permalinks
|
||||||
|
* are unique.
|
||||||
|
*/
|
||||||
|
|
||||||
|
visit(ast, 'heading', node => {
|
||||||
|
slugs['#' + slugger.slug(node.children[0].value)] = true;
|
||||||
|
if (node.depth > 3) return;
|
||||||
|
var { value } = node.children[0];
|
||||||
|
it('title ' + value + ' is unique', () => {
|
||||||
|
expect(seen.hasOwnProperty(value))
|
||||||
|
.toEqual(false, 'Title `' + value + '` should be unique');
|
||||||
|
seen[value] = true;
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('content', () => {
|
||||||
|
fs.readdirSync('./content').forEach(function(file) {
|
||||||
|
describe(file, () => {
|
||||||
|
var content = fs.readFileSync('./content/' + file, 'utf8');
|
||||||
|
var ast = remark.parse(content);
|
||||||
|
|
||||||
|
it('links are valid', function() {
|
||||||
|
visit(ast, 'link', node => {
|
||||||
|
if (node.href && node.href[0] === '#') {
|
||||||
|
expect(slugs[node.href])
|
||||||
|
.toExist('A link to ' + node.href + ' at ' +
|
||||||
|
node.position.start.line + ' of ' + file + ' was invalid');
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it('has h2 title', function() {
|
||||||
|
expect(ast.children[0].type).toEqual('heading');
|
||||||
|
expect(ast.children[0].depth).toEqual(2);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('has API description', function() {
|
||||||
|
expect(ast.children[1].type === 'paragraph' || ast.children[1].type === 'html').toEqual(true);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('has valid json', () => {
|
||||||
|
select(ast, 'code[lang=json]').forEach(node => {
|
||||||
|
expect(function() {
|
||||||
|
JSON.parse(node.value);
|
||||||
|
}).toNotThrow(null, 'a JSON code block at L:' +
|
||||||
|
node.position.start.line + ' of ' + file + ' was invalid');
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it('has valid javascript', () => {
|
||||||
|
select(ast, 'code[lang=javascript]').forEach(node => {
|
||||||
|
var messages = linter.verify(node.value);
|
||||||
|
expect(messages).toEqual([], 'a JS code block at L:' +
|
||||||
|
node.position.start.line + ' of ' + file + ' was invalid');
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
extractSections(ast).forEach(chunk => {
|
||||||
|
describe(getSectionTitle(chunk.children), function() {
|
||||||
|
it('has an endpoint and that endpoint has a valid method', () => {
|
||||||
|
var endpoint = select(chunk, 'code[lang=endpoint]');
|
||||||
|
expect(endpoint.length).toBeGreaterThan(0);
|
||||||
|
expect(endpoint[0].value.toString()).toMatch(/^(PUT|POST|GET|DELETE|PATCH)/);
|
||||||
|
});
|
||||||
|
if (!file.match(/(style|static_gl|maps|static_classic|uploads)/)) {
|
||||||
|
it('has python example', () => {
|
||||||
|
expect(select(chunk, 'code[lang=python]').length).toBeGreaterThan(0);
|
||||||
|
});
|
||||||
|
it('has js example', () => {
|
||||||
|
expect(select(chunk, 'code[lang=javascript]').length).toBeGreaterThan(0);
|
||||||
|
});
|
||||||
|
it('has curl example', () => {
|
||||||
|
expect(select(chunk, 'code[lang=curl]').length).toBeGreaterThan(0);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
Reference in New Issue
Block a user