This commit is contained in:
Tom MacWright
2016-03-07 11:20:04 -05:00
parent e90ffea98b
commit 3679b99300
12 changed files with 136 additions and 85 deletions

View File

@ -24,6 +24,12 @@ Documentation is written as Markdown files in the `content` directory, and is or
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. 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.
### Requirements
* Node v4 or higher
* NPM
* Git
To run the site locally: To run the site locally:
1. Clone this repository 1. Clone this repository

View File

@ -55,7 +55,7 @@ wobbles.list()
Creates a new, empty wobble. Creates a new, empty wobble.
```endpoint ```endpoint
POST /wobbles/v1/{username} wobbles:write POST /wobbles/v1/{username}
``` ```
#### Example request #### Example request
@ -114,7 +114,7 @@ Property | Description
Returns a single wobble. Returns a single wobble.
```endpoint ```endpoint
GET /wobbles/v1/{username}/{wobble_id} wobbles:read GET /wobbles/v1/{username}/{wobble_id}
``` ```
Retrieve information about an existing wobble. Retrieve information about an existing wobble.
@ -156,7 +156,7 @@ client.readWobble('wobble-id',
Updates the properties of a particular wobble. Updates the properties of a particular wobble.
```endpoint ```endpoint
PATCH /wobbles/v1/{username}/{wobble_id} wobbles:write PATCH /wobbles/v1/{username}/{wobble_id}
``` ```
#### Example request #### Example request
@ -217,7 +217,7 @@ Property | Description
Deletes a wobble, including all wibbles it contains. Deletes a wobble, including all wibbles it contains.
```endpoint ```endpoint
DELETE /wobbles/v1/{username}/{wobble_id} wobbles:write DELETE /wobbles/v1/{username}/{wobble_id}
``` ```
#### Example request #### Example request
@ -250,7 +250,7 @@ List all the wibbles in a wobble. The response body will be a
WobbleCollection. WobbleCollection.
```endpoint ```endpoint
GET /wobbles/v1/{username}/{wobble_id}/wibbles wobbles:read GET /wobbles/v1/{username}/{wobble_id}/wibbles
``` ```
#### Example request #### Example request
@ -304,7 +304,7 @@ 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. a wibble with that ID, a new wibble is created.
```endpoint ```endpoint
PUT /wobbles/v1/{username}/{wobble_id}/wibbles/{wibble_id} wobbles:write PUT /wobbles/v1/{username}/{wobble_id}/wibbles/{wibble_id}
``` ```
#### Example request #### Example request
@ -362,7 +362,7 @@ Property | Description
Retrieves a wibble in a wobble. Retrieves a wibble in a wobble.
```endpoint ```endpoint
GET /wobbles/v1/{username}/{wobble_id}/wibbles/{wibble_id} wobbles:read GET /wobbles/v1/{username}/{wobble_id}/wibbles/{wibble_id}
``` ```
#### Example request #### Example request
@ -403,7 +403,7 @@ wibble = wobbles.read_wibble(wobble_id, '2').json()
Removes a wibble from a wobble. Removes a wibble from a wobble.
```endpoint ```endpoint
DELETE /wobbles/v1/{username}/{wobble_id}/wibbles/{wibble_id} wobbles:write DELETE /wobbles/v1/{username}/{wobble_id}/wibbles/{wibble_id}
``` ```
#### Example request #### Example request

16
custom/content.js Normal file
View File

@ -0,0 +1,16 @@
var fs = require('fs');
/**
* This file exports the content of your website, as a bunch of concatenated
* Markdown files. By doing this explicitly, you can control the order
* of content without any level of abstraction.
*
* Using the brfs module, fs.readFileSync calls in this file are translated
* into strings of those files' content before the file is delivered to a
* browser: the content is read ahead-of-time and included in bundle.js.
*/
module.exports =
'# Introduction\n' +
fs.readFileSync('./content/introduction.md', 'utf8') + '\n' +
'# Example\n' +
fs.readFileSync('./content/example.md', 'utf8') + '\n';

61
custom/index.js Normal file
View File

@ -0,0 +1,61 @@
'use strict';
/**
* Brand names, in order to decreasing length, for different
* media queries.
*/
module.exports.brandNames = {
desktop: 'Wobble API Documentation',
tablet: 'Wobble API Docs',
mobile: 'API Docs'
};
/**
* Classes that define the top-left brand box.
*/
module.exports.brandClasses = 'fill-red';
/**
* Text for the link back to the linking website.
*/
module.exports.backLink = 'Back to wobbles.com';
/**
* Runs after highlighting code samples. You can use this
* hook to, for instance, highlight a token and link it
* to some canonical part of documentation.
*/
module.exports.postHighlight = function(html) {
return html;
};
/**
* Highlight tokens in endpoint URLs, optionally linking to documentation
* or adding detail. This is the equivalent of postHighlight but it
* operates on endpoint URLs only.
*/
function highlightTokens(str) {
return str.replace(/{[\w_]+}/g,
(str) => '<span class="strong">' + str + '</span>');
}
/**
* Transform endpoints given as strings in a highlighted block like
*
* ```endpoint
* GET /foo/bar
* ```
*
* Into HTML nodes that format those endpoints in nice ways.
*/
module.exports.transformURL = function(value) {
let parts = value.split(/\s+/);
return {
type: 'html',
value: `<div class='endpoint dark fill-dark round '>
<div class='round-left pad0y pad1x fill-lighten0 code small endpoint-method'>${parts[0]}</div>
<div class='pad0 code small endpoint-url'>${highlightTokens(parts[1])}</div>
</div>`
};
};

View File

@ -5,11 +5,12 @@ import RoundedToggle from './rounded_toggle';
import PureRenderMixin from 'react-pure-render/mixin'; import PureRenderMixin from 'react-pure-render/mixin';
import GithubSlugger from 'github-slugger'; import GithubSlugger from 'github-slugger';
import debounce from 'lodash.debounce'; import debounce from 'lodash.debounce';
import { brandNames, brandClasses } from '../../custom';
let slugger = new GithubSlugger(); let slugger = new GithubSlugger();
let slug = title => { slugger.reset(); return slugger.slug(title); }; let slug = title => { slugger.reset(); return slugger.slug(title); };
let languageOptions = ['curl', 'cli', 'python', 'javascript']; let languageOptions = ['cURL', 'CLI', 'Python', 'JavaScript'];
let debouncedReplaceState = debounce(hash => { let debouncedReplaceState = debounce(hash => {
window.history.replaceState('', '', hash); window.history.replaceState('', '', hash);
@ -45,7 +46,7 @@ var App = React.createClass({
return { return {
mqls: mqls, mqls: mqls,
queries: {}, queries: {},
language: 'curl', language: 'cURL',
activeSection: active, activeSection: active,
showNav: false showNav: false
}; };
@ -57,7 +58,7 @@ var App = React.createClass({
queries: { queries: {
desktop: true desktop: true
}, },
language: 'curl', language: 'cURL',
activeSection: '', activeSection: '',
showNav: false showNav: false
}; };
@ -126,11 +127,11 @@ var App = React.createClass({
{/* Content background */ } {/* Content background */ }
{!queries.mobile && <div className={`fixed-top fixed-right ${queries.desktop && 'space-left16'}`}> {!queries.mobile && <div className={`fixed-top fixed-right ${queries.desktop && 'space-left16'}`}>
<div className='fill-dark col6 pin-right'></div> <div className='fill-light col6 pin-right'></div>
</div>} </div>}
{/* Desktop nav */ } {/* Desktop nav */ }
{queries.desktop && <div className='space-top5 scroll-styled pad1 width16 sidebar fixed-left fill-light'> {queries.desktop && <div className='space-top5 scroll-styled pad1 width16 sidebar fixed-left fill-dark dark'>
<Navigation <Navigation
handleClick={this.handleClick} handleClick={this.handleClick}
activeSection={activeSection} activeSection={activeSection}
@ -142,12 +143,15 @@ var App = React.createClass({
<Content <Content
ast={ast} ast={ast}
queries={queries} queries={queries}
language={this.state.language}/> language={this.state.language.toLowerCase()}/>
</div> </div>
{/* Language toggle */ } {/* Language toggle */ }
<div className={`fixed-top dark ${queries.desktop && 'space-left16'}`}> <div className={`fixed-top ${queries.desktop && 'space-left16'}`}>
<div className={`events fill-dark2 pad1 col6 pin-topright ${queries.mobile && 'space-top5 fixed-topright'}`}> <div className={`events fill-light bottom-shadow pad1 col6 pin-topright ${queries.tablet && 'dark fill-blue'} ${queries.mobile && 'space-top5 fixed-topright'}`}>
<div className='space-right1 small quiet inline'>
Show examples in:
</div>
<RoundedToggle <RoundedToggle
options={languageOptions} options={languageOptions}
onChange={this.onChangeLanguage} onChange={this.onChangeLanguage}
@ -156,20 +160,20 @@ var App = React.createClass({
</div> </div>
{/* Header */ } {/* Header */ }
<div className={`fill-gray fixed-top ${queries.tablet ? 'pad1y pad2x col6' : 'pad0 width16'}`}> <div className={`fill-dark dark bottom-shadow fixed-top ${queries.tablet ? 'pad1y pad2x col6' : 'pad0 width16'}`}>
<a href='/' className='active space-top1 space-left1 pin-topleft icon round fill-blue dark pad0 mapbox'></a> <a href='/' className={`active space-top1 space-left1 pin-topleft icon round dark pad0 ${brandClasses}`}></a>
<div className={`strong small pad0 ${queries.mobile && 'space-left3'} ${queries.tablet ? 'space-left2' : 'space-left4 line-height15' }`}> <div className={`strong small pad0 ${queries.mobile && 'space-left3'} ${queries.tablet ? 'space-left2' : 'space-left4 line-height15' }`}>
{queries.desktop ? 'API Documentation' : {queries.desktop ? brandNames.desktop :
queries.mobile ? 'API Docs' : 'API Docs'} queries.mobile ? brandNames.mobile : brandNames.tablet}
</div> </div>
{queries.tablet && <div> {queries.tablet && <div>
<button <button
onClick={this.toggleNav} onClick={this.toggleNav}
className={`short quiet pin-topright micro button rcon ${showNav ? 'caret-up' : 'caret-down'} space-right1 space-top1`}> className={`short quiet pin-topright button rcon ${showNav ? 'caret-up' : 'caret-down'} space-right1 space-top1`}>
{activeSection} <span className='micro'>{activeSection}</span>
</button> </button>
{showNav && <div {showNav && <div
className='fixed-left fill-light pin-left col6 pad2 scroll-styled space-top5'> className='fixed-left keyline-top fill-dark pin-left col6 pad2 scroll-styled space-top5'>
<Navigation <Navigation
handleClick={this.handleClick} handleClick={this.handleClick}
activeSection={activeSection} activeSection={activeSection}

View File

@ -2,45 +2,10 @@ import React from 'react';
import Section from './section'; import Section from './section';
import PureRenderMixin from 'react-pure-render/mixin'; import PureRenderMixin from 'react-pure-render/mixin';
import GithubSlugger from 'github-slugger'; import GithubSlugger from 'github-slugger';
import { transformURL } from '../../custom';
let slugger = new GithubSlugger(); let slugger = new GithubSlugger();
let slug = title => { slugger.reset(); return slugger.slug(title); }; let slug = title => { slugger.reset(); return slugger.slug(title); };
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) { function chunkifyAST(ast, language) {
var preview = false; var preview = false;
return ast.children.reduce((chunks, node) => { return ast.children.reduce((chunks, node) => {
@ -72,7 +37,9 @@ function chunkifyAST(ast, language) {
right.push(node); right.push(node);
} }
} else if (node.lang === 'endpoint') { } else if (node.lang === 'endpoint') {
right.push(transformURL(node)); right.push(transformURL(node.value));
} else if (node.lang === null) {
left.push(node);
} }
} else if (node.type === 'heading' && node.depth >= 4) { } else if (node.type === 'heading' && node.depth >= 4) {
right.push(node); right.push(node);

View File

@ -1,6 +1,7 @@
import React from 'react'; import React from 'react';
import PureRenderMixin from 'react-pure-render/mixin'; import PureRenderMixin from 'react-pure-render/mixin';
import NavigationItem from './navigation_item'; import NavigationItem from './navigation_item';
import { backLink } from '../../custom';
function getAllInSectionFromChild(headings, idx) { function getAllInSectionFromChild(headings, idx) {
for (var i = idx; i > 0; i--) { for (var i = idx; i > 0; i--) {
@ -65,7 +66,7 @@ var Navigation = React.createClass({
if (child.depth === 1) { if (child.depth === 1) {
return (<div key={i} return (<div key={i}
onClick={this.handleClick} onClick={this.handleClick}
className='small pad0x strong space-top1'>{sectionName}</div>); className='small pad0x quiet space-top1'>{sectionName}</div>);
} else if (child.depth === 2) { } else if (child.depth === 2) {
return (<NavigationItem return (<NavigationItem
key={i} key={i}
@ -87,6 +88,7 @@ var Navigation = React.createClass({
} }
} }
})} })}
<a href='/' className='space-top2 pad1y dark keyline-top block small quiet'>{backLink}</a>
</div>); </div>);
} }
}); });

View File

@ -17,7 +17,7 @@ var NavigationItem = React.createClass({
return (<a return (<a
href={href} href={href}
onClick={this.onClick} onClick={this.onClick}
className={`line-height15 pad0x pad00y block ${active ? 'fill-darken0 quiet active round' : ''}`}> className={`line-height15 pad0x pad00y quiet block ${active ? 'fill-lighten0 round' : ''}`}>
{sectionName} {sectionName}
</a>); </a>);
} }

View File

@ -3,18 +3,16 @@ import remark from 'remark';
import remarkHTML from 'remark-html'; import remarkHTML from 'remark-html';
import remarkHighlight from 'remark-highlight.js'; import remarkHighlight from 'remark-highlight.js';
import PureRenderMixin from 'react-pure-render/mixin'; import PureRenderMixin from 'react-pure-render/mixin';
import { postHighlight } from '../../custom';
function renderHighlighted(nodes) { function renderHighlighted(nodes) {
return { return {
__html: remark() __html: postHighlight(remark()
.use(remarkHTML) .use(remarkHTML)
.stringify(remark().use(remarkHighlight).run({ .stringify(remark().use(remarkHighlight).run({
type: 'root', type: 'root',
children: nodes 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>`)
}; };
} }
@ -28,12 +26,12 @@ var Section = React.createClass({
let { left, right, preview } = chunk; let { left, right, preview } = chunk;
return (<div return (<div
data-title={chunk.title} data-title={chunk.title}
className={`section pad2y contain clearfix ${preview ? 'preview' : ''}`}> className={`keyline-top section contain clearfix ${preview ? 'preview' : ''}`}>
<div <div
className='col6 pad2x prose clip' className='space-bottom8 col6 pad2x prose clip'
dangerouslySetInnerHTML={renderHighlighted(left)} /> dangerouslySetInnerHTML={renderHighlighted(left)} />
{right.length > 0 && <div {right.length > 0 && <div
className='col6 pad2 prose dark space-top5 clip keyline-top fill-dark' className='space-bottom4 col6 pad2 prose clip fill-light space-top5'
dangerouslySetInnerHTML={renderHighlighted(right)} />} dangerouslySetInnerHTML={renderHighlighted(right)} />}
</div>); </div>);
} }

View File

@ -1,7 +0,0 @@
var fs = require('fs');
module.exports =
'# Introduction\n' +
fs.readFileSync('./content/introduction.md', 'utf8') + '\n' +
'# Example\n' +
fs.readFileSync('./content/example.md', 'utf8') + '\n';

View File

@ -4,9 +4,11 @@ import ReactDOM from 'react-dom';
import App from './components/app'; import App from './components/app';
import remark from 'remark'; import remark from 'remark';
import slug from 'remark-slug'; import slug from 'remark-slug';
import content from './content'; import content from '../custom/content';
var ast = remark().use(slug).run(remark().parse(content)); var ast = remark()
.use(slug)
.run(remark().parse(content));
ReactDOM.render( ReactDOM.render(
<App ast={ast} content={content} />, <App ast={ast} content={content} />,

View File

@ -3,10 +3,12 @@ import ReactDOMServer from 'react-dom/server';
import App from './components/app'; import App from './components/app';
import remark from 'remark'; import remark from 'remark';
import slug from 'remark-slug'; import slug from 'remark-slug';
import content from './content'; import content from '../custom/content';
import fs from 'fs'; import fs from 'fs';
var ast = remark().use(slug).run(remark().parse(content)); var ast = remark()
.use(slug)
.run(remark().parse(content));
var template = fs.readFileSync('./index.html', 'utf8'); var template = fs.readFileSync('./index.html', 'utf8');