diff --git a/assets/stylesheets/sections/devdocs.scss b/assets/stylesheets/sections/devdocs.scss
index 34ec49c7aa030e..00c8d56c676a0d 100644
--- a/assets/stylesheets/sections/devdocs.scss
+++ b/assets/stylesheets/sections/devdocs.scss
@@ -4,9 +4,7 @@
@import 'devdocs/style';
@import 'devdocs/design/style';
@import 'devdocs/design/syntax.scss';
+@import 'devdocs/gutenberg-components/style';
@import 'devdocs/gutenberg-blocks/style';
@import 'assets/stylesheets/sections/media';
-
-// Gutenberg components
-@import '../../../node_modules/@wordpress/components/build-style/style';
diff --git a/client/devdocs/controller.js b/client/devdocs/controller.js
index f824c4e96023cd..fc9ed5aed3bbea 100644
--- a/client/devdocs/controller.js
+++ b/client/devdocs/controller.js
@@ -166,7 +166,7 @@ const devdocs = {
// Gutenberg Components
gutenbergComponents: function( context, next ) {
context.primary = (
-
+
);
next();
},
diff --git a/client/devdocs/design/component-playground.jsx b/client/devdocs/design/component-playground.jsx
index 452cc87ab257f1..376ba8c5c64115 100644
--- a/client/devdocs/design/component-playground.jsx
+++ b/client/devdocs/design/component-playground.jsx
@@ -44,9 +44,6 @@ class ComponentPlayground extends Component {
const { section } = this.props;
let scope = null;
switch ( section ) {
- case 'gutenberg-components':
- scope = require( '@wordpress/components' );
- break;
case 'gutenberg-blocks':
scope = require( 'gutenberg-blocks' );
break;
diff --git a/client/devdocs/design/gutenberg-components.jsx b/client/devdocs/design/gutenberg-components.jsx
deleted file mode 100644
index af04ac488416ba..00000000000000
--- a/client/devdocs/design/gutenberg-components.jsx
+++ /dev/null
@@ -1,59 +0,0 @@
-/** @format */
-
-/**
- * External dependencies
- */
-import React from 'react';
-import classnames from 'classnames';
-import page from 'page';
-
-/**
- * Internal dependencies
- */
-import Collection from 'devdocs/design/search-collection';
-import DocumentHead from 'components/data/document-head';
-import HeaderCake from 'components/header-cake';
-import Main from 'components/main';
-import ReadmeViewer from 'components/readme-viewer';
-import { slugToCamelCase } from 'devdocs/docs-example/util';
-
-/**
- * Docs examples
- */
-import { Button } from 'gutenberg-components/examples';
-
-export default class extends React.Component {
- state = { filter: '' };
-
- backToAll = () => {
- page( '/devdocs/gutenberg-components/' );
- };
-
- render() {
- const { component } = this.props;
- const { filter } = this.state;
-
- const className = classnames( 'devdocs', 'devdocs__gutenberg-components', {
- 'is-single': component,
- 'is-list': ! component,
- } );
-
- return (
-
-
-
- { component ? (
-
- { slugToCamelCase( component ) }
-
- ) : (
-
- ) }
-
-
-
-
-
- );
- }
-}
diff --git a/client/devdocs/design/style.scss b/client/devdocs/design/style.scss
index f526c878d0b699..dd88e4d0abbb24 100644
--- a/client/devdocs/design/style.scss
+++ b/client/devdocs/design/style.scss
@@ -13,7 +13,7 @@
.design__playground {
margin: -24px;
- @include breakpoint( '>960px') {
+ @include breakpoint( '>960px' ) {
margin: -32px;
}
}
@@ -27,25 +27,25 @@
box-sizing: border-box;
color: $alert-red;
position: sticky;
- top: 0;
+ top: 0;
}
.react-live-error,
.design__preview {
padding: 24px;
- @include breakpoint( '>960px') {
+ @include breakpoint( '>960px' ) {
padding: 32px;
}
}
.design__preview {
position: relative;
- @include breakpoint( '>960px') {
+ @include breakpoint( '>960px' ) {
position: static;
}
}
-@include breakpoint( '>960px') {
+@include breakpoint( '>960px' ) {
.design__playground {
display: flex;
height: calc( 100vh - 47px );
@@ -71,8 +71,8 @@
.design__component-playground-clipboard {
padding: 0;
position: absolute;
- right: 10px;
- top: 10px;
+ right: 10px;
+ top: 10px;
}
.design__component-playground-show-code {
diff --git a/client/devdocs/design/test/component-playground.js b/client/devdocs/design/test/component-playground.js
index 159e72ff88b383..c2898b82d6e1f2 100644
--- a/client/devdocs/design/test/component-playground.js
+++ b/client/devdocs/design/test/component-playground.js
@@ -17,7 +17,6 @@ import ComponentPlayground from '../component-playground';
jest.mock( 'devdocs/design/playground-scope', () => 'PlaygroundScope' );
jest.mock( 'gutenberg-blocks', () => 'GutenbergBlocks' );
-jest.mock( '@wordpress/components', () => 'GutenbergComponents' );
describe( 'ComponentPlayground', () => {
test( 'LiveProvider should use the components scope by default', () => {
@@ -28,18 +27,6 @@ describe( 'ComponentPlayground', () => {
expect( liveProvider.props().scope ).toBe( 'PlaygroundScope' );
} );
- test(
- 'LiveProvider should use the Gutenberg components scope when section is Gutenberg' +
- ' components',
- () => {
- const wrapper = shallow(
-
- );
- const liveProvider = wrapper.find( LiveProvider );
- expect( liveProvider.props().scope ).toBe( 'GutenbergComponents' );
- }
- );
-
test( 'LiveProvider should use the Gutenberg blocks scope when section is Gutenberg blocks', () => {
const wrapper = shallow(
diff --git a/client/devdocs/gutenberg-components/example.jsx b/client/devdocs/gutenberg-components/example.jsx
new file mode 100644
index 00000000000000..f135f45bc06bd9
--- /dev/null
+++ b/client/devdocs/gutenberg-components/example.jsx
@@ -0,0 +1,77 @@
+/** @format */
+
+/**
+ * External dependencies
+ */
+import React from 'react';
+import * as components from '@wordpress/components';
+import { withState } from '@wordpress/compose';
+import { getSettings } from '@wordpress/date';
+import { addFilter } from '@wordpress/hooks';
+import { LiveError, LivePreview, LiveProvider } from 'react-live';
+import request from 'superagent';
+import codeBlocks from 'gfm-code-blocks';
+import classnames from 'classnames';
+import { kebabCase } from 'lodash';
+import PropTypes from 'prop-types';
+
+class Example extends React.Component {
+ state = {
+ code: null,
+ };
+
+ componentDidMount() {
+ this.getCode();
+ }
+
+ async getReadme() {
+ const readmeFilePath = `/node_modules/@wordpress/components/src/${
+ this.props.readmeFilePath
+ }/README.md`;
+
+ const { text } = await request.get( '/devdocs/service/content' ).query( {
+ path: readmeFilePath,
+ format: 'markdown',
+ } );
+ return text;
+ }
+
+ async getCode() {
+ const readme = await this.getReadme();
+
+ // Example to render is the first jsx code block that appears in the readme
+ let code = codeBlocks( readme ).find( block => 'jsx' === block.lang ).code;
+
+ // react-live cannot resolve imports in real time, so we get rid of them
+ // (dependencies will be injected via the scope property).
+ code = code.replace( /^.*import.*$/gm, '' );
+
+ code = `${ code } render( <${ this.props.render } /> );`;
+
+ this.setState( { code } );
+ }
+
+ render() {
+ const { code } = this.state;
+ const scope = {
+ ...components,
+ withState,
+ getSettings,
+ PropTypes,
+ addFilter,
+ };
+ const className = classnames(
+ 'devdocs__gutenberg-components-example',
+ `devdocs__gutenberg-components-example--${ kebabCase( this.props.component ) }`
+ );
+
+ return code ? (
+
+
+
+
+ ) : null;
+ }
+}
+
+export default Example;
diff --git a/client/devdocs/gutenberg-components/examples.json b/client/devdocs/gutenberg-components/examples.json
new file mode 100644
index 00000000000000..b0e171c3b4e121
--- /dev/null
+++ b/client/devdocs/gutenberg-components/examples.json
@@ -0,0 +1,90 @@
+[
+ { "component": "Autocomplete" },
+ { "component": "BaseControl" },
+ { "component": "Button" },
+ { "component": "ButtonGroup" },
+ { "component": "CheckboxControl" },
+ { "component": "ClipboardButton" },
+ { "component": "ColorIndicator" },
+ { "component": "ColorPalette" },
+ { "component": "Dashicon" },
+ { "component": "DateTimePicker", "readmeFilePath": "date-time" },
+ { "component": "Disabled" },
+ { "component": "Draggable" },
+ { "component": "DropZone" },
+ { "component": "Dropdown" },
+ { "component": "DropdownMenu" },
+ { "component": "ExternalLink" },
+ { "component": "FocusableIframe" },
+ { "component": "FontSizePicker" },
+ { "component": "FormFileUpload" },
+ { "component": "FormToggle" },
+ { "component": "FormTokenField" },
+ { "component": "IconButton" },
+ { "component": "KeyboardShortcuts" },
+ { "component": "MenuGroup" },
+ { "component": "MenuItem" },
+ { "component": "MenuItemsChoice" },
+ { "component": "Modal" },
+ {
+ "component": "navigateRegions",
+ "readmeFilePath": "higher-order/navigate-regions",
+ "render": "MyComponentWithNavigateRegions"
+ },
+ { "component": "NavigableContainer" },
+ { "component": "Notice" },
+ { "component": "Panel" },
+ { "component": "Placeholder" },
+ { "component": "Popover" },
+ { "component": "QueryControls" },
+ { "component": "RadioControl" },
+ { "component": "RangeControl" },
+ { "component": "ResponsiveWrapper" },
+ { "component": "SandBox", "readmeFilePath": "sandbox" },
+ { "component": "ScrollLock" },
+ { "component": "SelectControl" },
+ { "component": "SlotFillProvider", "readmeFilePath": "slot-fill" },
+ { "component": "Spinner" },
+ { "component": "TabPanel" },
+ { "component": "TextControl" },
+ { "component": "TextareaControl" },
+ { "component": "ToggleControl" },
+ { "component": "Toolbar" },
+ { "component": "Tooltip" },
+ { "component": "TreeSelect" },
+ {
+ "component": "withConstrainedTabbing",
+ "readmeFilePath": "higher-order/with-constrained-tabbing",
+ "render": "MyComponentWithConstrainedTabbing"
+ },
+ {
+ "component": "withFallbackStyles",
+ "readmeFilePath": "higher-order/with-fallback-styles",
+ "render": "MyComponentWithFallbackStyles"
+ },
+ {
+ "component": "withFilters",
+ "readmeFilePath": "higher-order/with-filters",
+ "render": "MyComponentWithFilters"
+ },
+ {
+ "component": "withFocusOutside",
+ "readmeFilePath": "higher-order/with-focus-outside",
+ "render": "MyComponentWithFocusOutside"
+ },
+ {
+ "component": "withFocusReturn",
+ "readmeFilePath": "higher-order/with-focus-return",
+ "render": "MyComponentWithFocusReturn"
+ },
+ {
+ "component": "withNotices",
+ "readmeFilePath": "higher-order/with-notices",
+ "render": "MyComponentWithNotices"
+ },
+ {
+ "component": "withSpokenMessages",
+ "readmeFilePath": "higher-order/with-spoken-messages",
+ "render": "MyComponentWithSpokenMessages"
+ }
+]
diff --git a/client/devdocs/gutenberg-components/index.jsx b/client/devdocs/gutenberg-components/index.jsx
new file mode 100644
index 00000000000000..b62394d81d99d8
--- /dev/null
+++ b/client/devdocs/gutenberg-components/index.jsx
@@ -0,0 +1,97 @@
+/** @format */
+
+/**
+ * External dependencies
+ */
+import React from 'react';
+import classnames from 'classnames';
+import page from 'page';
+import { get, trim } from 'lodash';
+
+/**
+ * Internal dependencies
+ */
+import Collection from 'devdocs/design/search-collection';
+import DocumentHead from 'components/data/document-head';
+import HeaderCake from 'components/header-cake';
+import Main from 'components/main';
+import ReadmeViewer from 'components/readme-viewer';
+import SearchCard from 'components/search-card';
+import { camelCaseToSlug, slugToCamelCase } from 'devdocs/docs-example/util';
+import GutenbergComponentExample from './example';
+import examples from './examples.json';
+
+const getExampleData = ( example ) => {
+ const componentName = get( example, 'component' );
+ const readmeFilePath = get( example, 'readmeFilePath', camelCaseToSlug( componentName ) );
+ const render = get( example, 'render', `My${ componentName }` );
+
+ return {
+ componentName,
+ readmeFilePath,
+ render,
+ };
+};
+
+export default class extends React.Component {
+ state = { filter: '' };
+
+ backToAll = () => {
+ page( '/devdocs/gutenberg-components/' );
+ };
+
+ onSearch = term => {
+ this.setState( { filter: trim( term || '' ).toLowerCase() } );
+ };
+
+ render() {
+ const { component } = this.props;
+ const { filter } = this.state;
+
+ const className = classnames( 'devdocs', 'devdocs__gutenberg-components', {
+ 'is-single': component,
+ 'is-list': ! component,
+ } );
+
+ return (
+
+
+
+ { component ? (
+
+ { slugToCamelCase( component ) }
+
+ ) : (
+
+
+
+
+ ) }
+
+
+ { examples.map( example => {
+ const {
+ componentName,
+ readmeFilePath,
+ render,
+ } = getExampleData( example );
+ return (
+
+ );
+ } ) }
+
+
+ );
+ }
+}
diff --git a/client/devdocs/gutenberg-components/style.scss b/client/devdocs/gutenberg-components/style.scss
new file mode 100644
index 00000000000000..003bd215923b3d
--- /dev/null
+++ b/client/devdocs/gutenberg-components/style.scss
@@ -0,0 +1,6 @@
+@import '../../../node_modules/@wordpress/components/build-style/style';
+
+// Styles the contenteditable div included in the Gutenberg Autocomplete component example as an input
+.devdocs__gutenberg-components-example--autocomplete [contenteditable] {
+ @extend %form-field;
+}
diff --git a/client/devdocs/gutenberg-components/test/example.jsx b/client/devdocs/gutenberg-components/test/example.jsx
new file mode 100644
index 00000000000000..b7075178e35e54
--- /dev/null
+++ b/client/devdocs/gutenberg-components/test/example.jsx
@@ -0,0 +1,75 @@
+/**
+ * @format
+ * @jest-environment jsdom
+ */
+
+/**
+ * External dependencies
+ */
+import React from 'react';
+import { shallow } from 'enzyme';
+import request from 'superagent';
+
+/**
+ * Internal dependencies
+ */
+import GutenbergComponentExample from '../example';
+
+jest.mock( 'superagent', () => ( {
+ get: jest.fn().mockReturnThis(),
+ query: jest.fn().mockResolvedValue( { text: 'foo' } ),
+} ) );
+
+describe( 'GutenbergComponentExample', () => {
+ test( 'should retrieve the code when mounted', () => {
+ const wrapper = shallow( , {
+ disableLifecycleMethods: true,
+ } );
+ wrapper.instance().getCode = jest.fn();
+ wrapper.instance().componentDidMount();
+ expect( wrapper.instance().getCode ).toHaveBeenCalled();
+ } );
+
+ test( 'should retrieve the README file from node_modules', async () => {
+ const wrapper = shallow( , {
+ disableLifecycleMethods: true,
+ } );
+ await wrapper.instance().getReadme();
+ expect( request.get ).toHaveBeenCalledWith( '/devdocs/service/content' );
+ expect( request.query ).toHaveBeenCalledWith( {
+ path: '/node_modules/@wordpress/components/src/bar/README.md',
+ format: 'markdown',
+ } );
+ } );
+
+ test( 'should get the code from the first jsx block', async () => {
+ const wrapper = shallow( , {
+ disableLifecycleMethods: true,
+ } );
+ const mockedReadme = '# test\n```jsx\nmy code\n```\n```jsx\ntest\n```';
+ wrapper.instance().getReadme = jest.fn().mockResolvedValue( mockedReadme );
+ await wrapper.instance().getCode();
+ expect( wrapper.state().code ).toContain( 'my code' );
+ expect( wrapper.state().code ).not.toContain( 'test' );
+ } );
+
+ test( 'should remove the imports from the example', async () => {
+ const wrapper = shallow( , {
+ disableLifecycleMethods: true,
+ } );
+ const mockedReadme = '```jsx\nimport test\nmy code\n```';
+ wrapper.instance().getReadme = jest.fn().mockResolvedValue( mockedReadme );
+ await wrapper.instance().getCode();
+ expect( wrapper.state().code ).not.toContain( 'import test' );
+ } );
+
+ test( 'should render the given component', async () => {
+ const wrapper = shallow( , {
+ disableLifecycleMethods: true,
+ } );
+ const mockedReadme = '```jsx\nmy code\n```';
+ wrapper.instance().getReadme = jest.fn().mockResolvedValue( mockedReadme );
+ await wrapper.instance().getCode();
+ expect( wrapper.state().code ).toContain( 'render( )' );
+ } );
+} );
diff --git a/client/devdocs/design/test/gutenberg-components.js b/client/devdocs/gutenberg-components/test/index.js
similarity index 84%
rename from client/devdocs/design/test/gutenberg-components.js
rename to client/devdocs/gutenberg-components/test/index.js
index ff280129c60429..4de95c2657cfe2 100644
--- a/client/devdocs/design/test/gutenberg-components.js
+++ b/client/devdocs/gutenberg-components/test/index.js
@@ -13,10 +13,12 @@ import page from 'page';
/**
* Internal dependencies
*/
-import GutenbergComponents from '../gutenberg-components';
+import GutenbergComponents from '../';
import HeaderCake from 'components/header-cake';
import ReadmeViewer from 'components/readme-viewer';
import Collection from 'devdocs/design/search-collection';
+import GutenbergComponentExample from '../example';
+import examplesList from '../examples.json';
jest.mock( 'page' );
@@ -26,26 +28,24 @@ describe( 'GutenbergComponents', () => {
const headerCake = wrapper.find( HeaderCake );
const readmeViewer = wrapper.find( ReadmeViewer );
const collection = wrapper.find( Collection );
+ const examples = wrapper.find( GutenbergComponentExample );
expect( wrapper.hasClass( 'is-list' ) ).toBe( true );
expect( headerCake ).toHaveLength( 0 );
expect( readmeViewer ).toHaveLength( 1 );
expect( readmeViewer.props().readmeFilePath ).toBe(
'/client/devdocs/gutenberg-components/README.md'
);
- expect( collection ).toHaveLength( 1 );
- expect( collection.children() ).toHaveLength( 1 );
+ expect( collection.children() ).toEqual( examples );
+ expect( examples ).toHaveLength( examplesList.length );
} );
test( 'should render a single component when a component is given', () => {
const wrapper = shallow( );
const headerCake = wrapper.find( HeaderCake );
const readmeViewer = wrapper.find( ReadmeViewer );
- const collection = wrapper.find( Collection );
expect( wrapper.hasClass( 'is-single' ) ).toBe( true );
expect( headerCake ).toHaveLength( 1 );
expect( readmeViewer ).toHaveLength( 0 );
- expect( collection ).toHaveLength( 1 );
- expect( collection.children() ).toHaveLength( 1 );
} );
test( 'should go back when clicking in HeaderCake', () => {
diff --git a/client/devdocs/index.js b/client/devdocs/index.js
index 48da16b88a2c87..334b71be0bb793 100644
--- a/client/devdocs/index.js
+++ b/client/devdocs/index.js
@@ -72,15 +72,15 @@ export default function() {
page( '/devdocs/start', controller.pleaseLogIn, makeLayout, clientRender );
page( '/devdocs/welcome', controller.sidebar, controller.welcome, makeLayout, clientRender );
- page(
- '/devdocs/gutenberg-components/:component?',
- controller.sidebar,
- controller.gutenbergComponents,
- makeLayout,
- clientRender
- );
-
if ( config.isEnabled( 'devdocs/gutenberg-blocks' ) ) {
+ page(
+ '/devdocs/gutenberg-components/:component?',
+ controller.sidebar,
+ controller.gutenbergComponents,
+ makeLayout,
+ clientRender
+ );
+
page(
'/devdocs/gutenberg-blocks/:block*',
controller.sidebar,
diff --git a/client/devdocs/sidebar.jsx b/client/devdocs/sidebar.jsx
index 7e02123fad925b..a934fe9b089fd9 100644
--- a/client/devdocs/sidebar.jsx
+++ b/client/devdocs/sidebar.jsx
@@ -104,13 +104,15 @@ export default class DevdocsSidebar extends React.PureComponent {
link="/devdocs/blocks"
selected={ this.isItemSelected( '/devdocs/blocks', false ) }
/>
-
+ { isEnabled( 'devdocs/gutenberg-blocks' ) && (
+
+ ) }
{ isEnabled( 'devdocs/gutenberg-blocks' ) && (
-
-
-
-
-
-
-
-
-
-
- ),
- };
-
- render() {
- return this.props.exampleCode;
- }
-}
diff --git a/client/gutenberg-components/examples.js b/client/gutenberg-components/examples.js
deleted file mode 100644
index 30e3d6c50d0504..00000000000000
--- a/client/gutenberg-components/examples.js
+++ /dev/null
@@ -1 +0,0 @@
-export Button from './button/docs/example';
diff --git a/docs/components.md b/docs/components.md
index c3fca5c9381866..a403eb08c57554 100644
--- a/docs/components.md
+++ b/docs/components.md
@@ -10,8 +10,6 @@ You will encounter the following types of components in Calypso:
* [UI components](../client/components/README.md) (UI primitives)
* [Blocks](../client/blocks/README.md) (components which are connected to state, or otherwise directly represent application entities)
-* [Gutenberg components](../client/gutenberg-components/README.md) (components included in the
-[`@wordpress/components`](https://www.npmjs.com/package/@wordpress/components) package)
* [Query components](./our-approach-to-data.md#query-components) (which handle data querying but don’t render anything)
* Higher-order components (which encapsulate and provide functionality)
* Section components (which are domain specific and not meant to be reused)
diff --git a/npm-shrinkwrap.json b/npm-shrinkwrap.json
index 043e3eb059310c..2db5a8a3ca5434 100644
--- a/npm-shrinkwrap.json
+++ b/npm-shrinkwrap.json
@@ -6952,6 +6952,19 @@
"safe-buffer": "^5.1.1"
}
},
+ "gfm-code-block-regex": {
+ "version": "1.0.0",
+ "resolved": "https://registry.npmjs.org/gfm-code-block-regex/-/gfm-code-block-regex-1.0.0.tgz",
+ "integrity": "sha1-u4PH1ihOa1ty+gIZilisDSViFdI="
+ },
+ "gfm-code-blocks": {
+ "version": "1.0.0",
+ "resolved": "https://registry.npmjs.org/gfm-code-blocks/-/gfm-code-blocks-1.0.0.tgz",
+ "integrity": "sha1-YU0hBZuETGu8nViMCJslxOi8zw0=",
+ "requires": {
+ "gfm-code-block-regex": "^1.0.0"
+ }
+ },
"glob": {
"version": "7.1.2",
"resolved": "https://registry.npmjs.org/glob/-/glob-7.1.2.tgz",
diff --git a/package.json b/package.json
index a28c99c75b407e..740f9f32f4197c 100644
--- a/package.json
+++ b/package.json
@@ -40,6 +40,7 @@
"@wordpress/compose": "1.0.4",
"@wordpress/core-data": "1.1.1",
"@wordpress/data": "1.2.1",
+ "@wordpress/date": "1.0.3",
"@wordpress/deprecated": "1.0.3",
"@wordpress/editor": "2.0.1",
"@wordpress/element": "1.0.4",
@@ -105,6 +106,7 @@
"flux": "3.1.3",
"fuse.js": "3.2.1",
"get-video-id": "3.1.0",
+ "gfm-code-blocks": "1.0.0",
"globby": "8.0.1",
"gridicons": "3.0.1",
"gzip-size": "5.0.0",
diff --git a/server/devdocs/index.js b/server/devdocs/index.js
index 91b4503a88ae12..b30e2ad60eeae8 100644
--- a/server/devdocs/index.js
+++ b/server/devdocs/index.js
@@ -209,9 +209,11 @@ module.exports = function() {
response.json( listDocs( files.split( ',' ) ) );
} );
- // return the HTML content of a document (assumes that the document is in markdown format)
+ // return the content of a document in the given format (assumes that the document is in
+ // markdown format)
app.get( '/devdocs/service/content', ( request, response ) => {
let path = request.query.path;
+ const format = request.query.format || 'html';
if ( ! path ) {
response
@@ -237,7 +239,7 @@ module.exports = function() {
const fileContents = fs.readFileSync( path, { encoding: 'utf8' } );
- response.send( marked( fileContents ) );
+ response.send( 'html' === format ? marked( fileContents ) : fileContents );
} );
// return json for the components usage stats