Skip to content

Commit

Permalink
Feat: Better anchors navigation with unique slugs (#318)
Browse files Browse the repository at this point in the history
* Fixes anchors for sections with multiple words in name
* Fixes the case when components or sections with same name could not be resolved by anchor
* Adds anchor navigation to the tittle of section/component
* Use `<section>` tag for sections
* Use `describe` in utils tests
* Use only react-docgen-displayname-handler to read component name
* Do not try to detect component name at runtime
* Remove nameFallback
* Put examples inside props
* Pass actual component name to default example instead of fallback
* Explicitly support only one component in props-loader

BREAKING CHANGE:

handlers config option is now function instead of array.
  • Loading branch information
okonet authored and sapegin committed Feb 17, 2017
1 parent baf7fec commit 468cf75
Show file tree
Hide file tree
Showing 46 changed files with 823 additions and 360 deletions.
8 changes: 4 additions & 4 deletions docs/Configuration.md
Original file line number Diff line number Diff line change
Expand Up @@ -120,15 +120,15 @@ module.exports = {

#### `handlers`

Type: `Function[]`, optional, default: [[react-docgen-displayname-handler](https://github.com/nerdlabs/react-docgen-displayname-handler)]
Type: `Function`, optional, default: [[react-docgen-displayname-handler](https://github.com/nerdlabs/react-docgen-displayname-handler)]

Functions used to process the discovered components and generate documentation objects. Default behaviors include discovering component documentation blocks, prop types, and defaults. If setting this property, it is best to build from the default `react-docgen` handler list, such as in the example below. See the [react-docgen handler documentation](https://github.com/reactjs/react-docgen#handlers) for more information about handlers.
Function that returns functions used to process the discovered components and generate documentation objects. Default behaviors include discovering component documentation blocks, prop types, and defaults. If setting this property, it is best to build from the default `react-docgen` handler list, such as in the example below. See the [react-docgen handler documentation](https://github.com/reactjs/react-docgen#handlers) for more information about handlers.

Also note that the default handler, `react-docgen-displayname-handler` should be included to better support higher order components.

```javascript
module.exports = {
handlers: require('react-docgen').defaultHandlers.concat(
handlers: componentPath => require('react-docgen').defaultHandlers.concat(
(documentation, path) => {
// Calculate a display name for components based upon the declared class name.
if (path.value.type === 'ClassDeclaration' && path.value.id.type === 'Identifier') {
Expand All @@ -147,7 +147,7 @@ module.exports = {
},

// To better support higher order components
require('react-docgen-displayname-handler').default,
require('react-docgen-displayname-handler').createDisplayNameHandler(componentPath),
)
};
```
Expand Down
29 changes: 24 additions & 5 deletions loaders/__tests__/props-loader.spec.js
Original file line number Diff line number Diff line change
@@ -1,28 +1,47 @@
import vm from 'vm';
import { readFileSync } from 'fs';
import config from '../../scripts/schemas/config';
import propsLoader from '../props-loader';

const _styleguidist = {
handlers: config.handlers.default,
getExampleFilename: config.getExampleFilename.default,
};

it('should return valid, parsable JS', () => {
const file = './test/components/Button/Button.js';
const result = propsLoader.call({
request: file,
_styleguidist: {},
_styleguidist,
}, readFileSync(file, 'utf8'));
expect(result).toBeTruthy();
expect(() => new Function(result)).not.toThrowError(SyntaxError); // eslint-disable-line no-new-func
expect(new vm.Script(result)).not.toThrowError(SyntaxError);
});

it('should extract doclets', () => {
const file = './test/components/Placeholder/Placeholder.js';
const result = propsLoader.call({
request: file,
_styleguidist: {},
_styleguidist,
}, readFileSync(file, 'utf8'));
expect(result).toBeTruthy();

expect(() => new Function(result)).not.toThrowError(SyntaxError); // eslint-disable-line no-new-func
expect(result.includes('getImageUrl')).toBe(true);
expect(new vm.Script(result)).not.toThrowError(SyntaxError);
expect(result.includes('makeABarrelRoll')).toBe(false);
expect(result).toMatch('getImageUrl');
expect(result).toMatch(/'see': '\{@link link\}'/);
expect(result).toMatch(/'link': 'link'/);
expect(result).toMatch(/require\('!!.*?\/loaders\/examples-loader\.js!\.\/examples.md'\)/);
});

it('should attach examples from Markdown file', () => {
const file = './test/components/Button/Button.js';
const result = propsLoader.call({
request: file,
_styleguidist,
}, readFileSync(file, 'utf8'));
expect(result).toBeTruthy();

expect(new vm.Script(result)).not.toThrowError(SyntaxError);
expect(result).toMatch(/require\('!!.*?\/loaders\/examples-loader\.js!test\/components\/Button\/Readme.md'\)/);
});
29 changes: 14 additions & 15 deletions loaders/props-loader.js
Original file line number Diff line number Diff line change
@@ -1,11 +1,12 @@
'use strict';

const path = require('path');
const castArray = require('lodash/castArray');
const isArray = require('lodash/isArray');
const reactDocs = require('react-docgen');
const generate = require('escodegen').generate;
const toAst = require('to-ast');
const getPropsCode = require('./utils/getProps');
const getExamples = require('./utils/getExamples');
const getProps = require('./utils/getProps');

/* eslint-disable no-console */

Expand All @@ -18,32 +19,30 @@ module.exports = function(source) {
const file = this.request.split('!').pop();
const config = this._styleguidist;

const defaultParser = (filePath, source) => reactDocs.parse(source, config.resolver, config.handlers);
const defaultParser = (filePath, source) => reactDocs.parse(source, config.resolver, config.handlers(file));
const propsParser = config.propsParser || defaultParser;

let parsedProps;
let props = {};
try {
parsedProps = propsParser(file, source);
props = propsParser(file, source);
}
/* istanbul ignore next */
catch (exception) {
parsedProps = [];
console.log('Error when parsing', path.relative(process.cwd(), file));
console.log(exception.toString());
console.log();
}

parsedProps = castArray(parsedProps);
// Support only one component
if (isArray(props)) {
props = props[0];
}

// Keep only public methods
parsedProps = parsedProps.map(prop => Object.assign(prop, {
methods: prop.methods.filter(method => {
const doclets = method.docblock && reactDocs.utils.docblock.getDoclets(method.docblock);
return doclets && doclets.public;
}),
}));
props = getProps(props);

const props = parsedProps.map(getPropsCode);
// Examples from Markdown file
const examplesFile = config.getExampleFilename(file);
props.examples = getExamples(examplesFile, props.displayName, config.defaultExample);

return `
if (module.hot) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -9,8 +9,8 @@ Array [
Object {
"components": Array [
Object {
"examples": "require()",
"filepath": "components/Button/Button.js",
"hasExamples": "require()",
},
],
"name": "Components",
Expand All @@ -23,8 +23,8 @@ Array [
Object {
"components": Array [
Object {
"examples": "require()",
"filepath": "components/Modal/Modal.js",
"hasExamples": "require()",
},
],
"name": "Nested",
Expand Down
Original file line number Diff line number Diff line change
@@ -1,12 +1,12 @@
exports[`test should return only components with example file 1`] = `
Array [
Object {
"examples": "require()",
"filepath": "components/Button/Button.js",
"hasExamples": true,
},
Object {
"examples": "require()",
"filepath": "components/Modal/Modal.js",
"hasExamples": true,
},
]
`;
Original file line number Diff line number Diff line change
@@ -1,24 +1,22 @@
exports[`test getComponents() should return an object for components 1`] = `
Array [
Object {
"examples": null,
"filepath": "../Foo.js",
"hasExamples": false,
"module": RequireStatement {
"filepath": "Foo.js",
},
"nameFallback": "Foo",
"pathLine": "../Foo.js",
"props": RequireStatement {
"filepath": "!!<rootDir>/loaders/props-loader.js!Foo.js",
},
},
Object {
"examples": null,
"filepath": "../Bar.js",
"hasExamples": false,
"module": RequireStatement {
"filepath": "Bar.js",
},
"nameFallback": "Bar",
"pathLine": "../Bar.js",
"props": RequireStatement {
"filepath": "!!<rootDir>/loaders/props-loader.js!Bar.js",
Expand Down
61 changes: 55 additions & 6 deletions loaders/utils/__tests__/__snapshots__/getProps.spec.js.snap
Original file line number Diff line number Diff line change
@@ -1,4 +1,38 @@
exports[`test getProps() should return an object for props 1`] = `
exports[`test should highlight code in description (fenced code block) 1`] = `
Object {
"description": "The only true button.
<span class=\"hljs-selector-tag\">alert</span>(<span class=\"hljs-string\">\'Hello world\'</span>);
",
"doclets": Object {},
"methods": Array [],
}
`;
exports[`test should highlight code in description (regular code block) 1`] = `
Object {
"description": "The only true button.
<span class=\"hljs-selector-tag\">alert</span>(<span class=\"hljs-string\">\'Hello world\'</span>);
",
"doclets": Object {},
"methods": Array [],
}
`;
exports[`test should remove non-public methods 1`] = `
Object {
"doclets": Object {},
"methods": Array [
Object {
"docblock": "Public method.
@public",
},
],
}
`;
exports[`test should return an object for props 1`] = `
Object {
"description": "The only true button.
",
Expand All @@ -19,21 +53,23 @@ Object {
}
`;
exports[`test getProps() should return an object for props with doclets 1`] = `
exports[`test should return an object for props with doclets 1`] = `
Object {
"description": "The only true button.
",
"doclets": Object {},
"example": RequireStatement {
"filepath": "!!<rootDir>/loaders/examples-loader.js!example.md
"doclets": Object {
"bar": "Bar
",
"foo": "Foo",
},
"methods": Array [],
}
`;
exports[`test getProps() should return an object for props without description 1`] = `
exports[`test should return an object for props without description 1`] = `
Object {
"doclets": Object {},
"methods": Array [],
"props": Object {
"children": Object {
"description": "Button label.",
Expand All @@ -43,3 +79,16 @@ Object {
},
}
`;
exports[`test should return require statement for @example doclet 1`] = `
Object {
"description": "The only true button.
",
"doclets": Object {},
"example": RequireStatement {
"filepath": "!!<rootDir>/loaders/examples-loader.js!example.md
",
},
"methods": Array [],
}
`;
30 changes: 6 additions & 24 deletions loaders/utils/__tests__/__snapshots__/getSections.spec.js.snap
Original file line number Diff line number Diff line change
Expand Up @@ -11,42 +11,33 @@ Array [
Object {
"components": Array [
Object {
"examples": RequireStatement {
"filepath": "!!<rootDir>/loaders/examples-loader.js!<rootDir>/test/components/Button/Button.js",
},
"filepath": "components/Button/Button.js",
"hasExamples": true,
"module": RequireStatement {
"filepath": "<rootDir>/test/components/Button/Button.js",
},
"nameFallback": "Button",
"pathLine": "components/Button/Button.js",
"props": RequireStatement {
"filepath": "!!<rootDir>/loaders/props-loader.js!<rootDir>/test/components/Button/Button.js",
},
},
Object {
"examples": RequireStatement {
"filepath": "!!<rootDir>/loaders/examples-loader.js!<rootDir>/test/components/Placeholder/Placeholder.js",
},
"filepath": "components/Placeholder/Placeholder.js",
"hasExamples": true,
"module": RequireStatement {
"filepath": "<rootDir>/test/components/Placeholder/Placeholder.js",
},
"nameFallback": "Placeholder",
"pathLine": "components/Placeholder/Placeholder.js",
"props": RequireStatement {
"filepath": "!!<rootDir>/loaders/props-loader.js!<rootDir>/test/components/Placeholder/Placeholder.js",
},
},
Object {
"examples": RequireStatement {
"filepath": "!!<rootDir>/loaders/examples-loader.js!<rootDir>/test/components/RandomButton/RandomButton.js",
},
"filepath": "components/RandomButton/RandomButton.js",
"hasExamples": true,
"module": RequireStatement {
"filepath": "<rootDir>/test/components/RandomButton/RandomButton.js",
},
"nameFallback": "RandomButton",
"pathLine": "components/RandomButton/RandomButton.js",
"props": RequireStatement {
"filepath": "!!<rootDir>/loaders/props-loader.js!<rootDir>/test/components/RandomButton/RandomButton.js",
Expand All @@ -64,42 +55,33 @@ exports[`test processSection() should return an object for section with componen
Object {
"components": Array [
Object {
"examples": RequireStatement {
"filepath": "!!<rootDir>/loaders/examples-loader.js!<rootDir>/test/components/Button/Button.js",
},
"filepath": "components/Button/Button.js",
"hasExamples": true,
"module": RequireStatement {
"filepath": "<rootDir>/test/components/Button/Button.js",
},
"nameFallback": "Button",
"pathLine": "components/Button/Button.js",
"props": RequireStatement {
"filepath": "!!<rootDir>/loaders/props-loader.js!<rootDir>/test/components/Button/Button.js",
},
},
Object {
"examples": RequireStatement {
"filepath": "!!<rootDir>/loaders/examples-loader.js!<rootDir>/test/components/Placeholder/Placeholder.js",
},
"filepath": "components/Placeholder/Placeholder.js",
"hasExamples": true,
"module": RequireStatement {
"filepath": "<rootDir>/test/components/Placeholder/Placeholder.js",
},
"nameFallback": "Placeholder",
"pathLine": "components/Placeholder/Placeholder.js",
"props": RequireStatement {
"filepath": "!!<rootDir>/loaders/props-loader.js!<rootDir>/test/components/Placeholder/Placeholder.js",
},
},
Object {
"examples": RequireStatement {
"filepath": "!!<rootDir>/loaders/examples-loader.js!<rootDir>/test/components/RandomButton/RandomButton.js",
},
"filepath": "components/RandomButton/RandomButton.js",
"hasExamples": true,
"module": RequireStatement {
"filepath": "<rootDir>/test/components/RandomButton/RandomButton.js",
},
"nameFallback": "RandomButton",
"pathLine": "components/RandomButton/RandomButton.js",
"props": RequireStatement {
"filepath": "!!<rootDir>/loaders/props-loader.js!<rootDir>/test/components/RandomButton/RandomButton.js",
Expand Down
Original file line number Diff line number Diff line change
@@ -1,13 +1,10 @@
exports[`test processComponent() should return an object for section with content 1`] = `
Object {
"examples": RequireStatement {
"filepath": "!!<rootDir>/loaders/examples-loader.js!Readme.md",
},
"filepath": "../../../pizza.js",
"hasExamples": true,
"module": RequireStatement {
"filepath": "pizza.js",
},
"nameFallback": "pizza",
"pathLine": "../../../pizza.js",
"props": RequireStatement {
"filepath": "!!<rootDir>/loaders/props-loader.js!pizza.js",
Expand Down
Loading

0 comments on commit 468cf75

Please sign in to comment.