Repository: styleguidist/react-styleguidist (ts)

Issue:
Line numbers enable configuration needed
Is it possible provide a showLineNumbers configuration flag in styleguide.config.js to enable/disable showing line numbers in the code editor?

Pull request:
feat(editor): Passes all codeMirror options to editor
All you now pass inside codeMirrorOptions in your styleguide config will be passed to codeMirror.
I left highlight theme out because I didn't want to cause a breaking change

closes #648

Patch:
diff --git a/docs/Configuration.md b/docs/Configuration.md
index 795aaffc..def70500 100644
--- a/docs/Configuration.md
+++ b/docs/Configuration.md
@@ -14,10 +14,10 @@ By default, Styleguidist will look for `styleguide.config.js` file in your proje
 * [`configureServer`](#configureserver)
 * [`dangerouslyUpdateWebpackConfig`](#dangerouslyupdatewebpackconfig)
 * [`defaultExample`](#defaultexample)
+* [`editorConfig`](#editorConfig)
 * [`getComponentPathLine`](#getcomponentpathline)
 * [`getExampleFilename`](#getexamplefilename)
 * [`handlers`](#handlers)
-* [`highlightTheme`](#highlighttheme)
 * [`ignore`](#ignore)
 * [`logger`](#logger)
 * [`previewDelay`](#previewdelay)
@@ -168,6 +168,12 @@ module.exports = {
 }
 ```
 
+#### `editorConfig`
+
+Type: `Object`, default: [scripts/schemas/config.js](https://github.com/styleguidist/react-styleguidist/tree/master/scripts/schemas/config.js#L101)
+
+Source code editor options, see [CodeMirror docs](https://codemirror.net/doc/manual.html#config) for all available options.
+
 #### `getExampleFilename`
 
 Type: `Function`, default: finds `Readme.md` or `ComponentName.md` in the component folder
@@ -227,12 +233,6 @@ module.exports = {
 }
 ```
 
-#### `highlightTheme`
-
-Type: `String`, default: `base16-light`
-
-[CodeMirror theme](http://codemirror.net/demo/theme.html) name to use for syntax highlighting in the editor.
-
 #### `ignore`
 
 Type: `String[]`, default: `['**/__tests__/**', '**/*.test.js', '**/*.test.jsx', '**/*.spec.js', '**/*.spec.jsx']`
diff --git a/jsconfig.json b/jsconfig.json
new file mode 100644
index 00000000..44cb87e8
--- /dev/null
+++ b/jsconfig.json
@@ -0,0 +1,13 @@
+{
+  "compilerOptions": {
+    "lib": [
+      "dom",
+      "esnext"
+    ],
+    "jsx": "react",
+    "baseUrl": "./src"
+  },
+  "exclude": [
+    "node_modules"
+  ]
+}
diff --git a/loaders/styleguide-loader.js b/loaders/styleguide-loader.js
index cf7645fe..7ce57f36 100644
--- a/loaders/styleguide-loader.js
+++ b/loaders/styleguide-loader.js
@@ -24,6 +24,7 @@ const CLIENT_CONFIG_OPTIONS = [
 	'theme',
 	'styles',
 	'compilerConfig',
+	'editorConfig',
 ];
 
 module.exports = function() {};
diff --git a/package.json b/package.json
index 52f68b01..621fdb22 100644
--- a/package.json
+++ b/package.json
@@ -1,7 +1,7 @@
 {
   "name": "react-styleguidist",
   "description": "React components style guide generator",
-  "version": "0.0.0-development",
+  "version": "6.1.0",
   "homepage": "https://github.com/styleguidist/react-styleguidist",
   "author": {
     "name": "Artem Sapegin",
diff --git a/scripts/logger.js b/scripts/logger.js
index 0f60a16b..84c7742d 100644
--- a/scripts/logger.js
+++ b/scripts/logger.js
@@ -2,13 +2,14 @@
 /* eslint-disable no-console */
 
 const _ = require('lodash/fp');
+const chalk = require('chalk');
 const logger = require('glogg')('rsg');
 
 const format = message => message.trim() + '\n';
 
 const printers = {
 	info: message => console.log(format(message)),
-	warn: message => console.warn(`Warning: ${format(message)}`),
+	warn: message => console.warn(chalk.yellow(`Warning: ${format(message)}`)),
 	debug: message => console.log(format(message)),
 };
 
diff --git a/scripts/schemas/config.js b/scripts/schemas/config.js
index db6c4390..bb1b8d5d 100644
--- a/scripts/schemas/config.js
+++ b/scripts/schemas/config.js
@@ -93,6 +93,27 @@ module.exports = {
 	highlightTheme: {
 		type: 'string',
 		default: 'base16-light',
+		deprecated: 'Use the theme property in the editorConfig option instead',
+	},
+	editorConfig: {
+		type: 'object',
+		process: (value, config) => {
+			const defaults = {
+				theme: 'base16-light',
+				mode: 'jsx',
+				lineWrapping: true,
+				smartIndent: false,
+				matchBrackets: true,
+				viewportMargin: Infinity,
+				lineNumbers: false,
+			};
+			return Object.assign(
+				{},
+				defaults,
+				config.highlightTheme && { theme: config.highlightTheme },
+				value
+			);
+		},
 	},
 	logger: {
 		type: 'object',
diff --git a/scripts/utils/sanitizeConfig.js b/scripts/utils/sanitizeConfig.js
index a1f79bf4..afe1d91a 100644
--- a/scripts/utils/sanitizeConfig.js
+++ b/scripts/utils/sanitizeConfig.js
@@ -89,7 +89,7 @@ module.exports = function sanitizeConfig(config, schema, rootDir) {
 				throw new StyleguidistError(message);
 			}
 		} else if (props.deprecated) {
-			logger.warn(`${chalk.bold(key)} config option is deprecated. ${props.deprecated}`);
+			logger.warn(`${key} config option is deprecated. ${props.deprecated}`);
 		} else if (props.removed) {
 			throw new StyleguidistError(`${chalk.bold(key)} config option was removed. ${props.removed}`);
 		}
diff --git a/src/index.js b/src/index.js
index 8f88d30e..424a5a5b 100644
--- a/src/index.js
+++ b/src/index.js
@@ -1,51 +1,18 @@
 /* eslint-disable import/first */
 
 import './polyfills';
-import React from 'react';
-import ReactDOM from 'react-dom';
-import slots from 'rsg-components/slots';
-import StyleGuide from 'rsg-components/StyleGuide';
-import getPageTitle from './utils/getPageTitle';
-import getRouteData from './utils/getRouteData';
-import globalizeComponents from './utils/globalizeComponents';
 import './styles';
+import ReactDOM from 'react-dom';
+import renderStyleguide from './utils/renderStyleguide';
 
 // Examples code revision to rerender only code examples (not the whole page) when code changes
 let codeRevision = 0;
 
-function renderStyleguide() {
+const render = () => {
 	// eslint-disable-next-line import/no-unresolved
 	const styleguide = require('!!../loaders/styleguide-loader!./index.js');
-
-	const { hash } = window.location;
-
-	const { sections, displayMode } = getRouteData(styleguide.sections, hash);
-
-	// Update page title
-	document.title = getPageTitle(sections, styleguide.config.title, displayMode);
-
-	// If the current hash location was set to just `/` (e.g. when navigating back from isolated view to overview)
-	// replace the URL with one without hash, to present the user with a single address of the overview screen
-	if (hash === '#/') {
-		const url = window.location.pathname + window.location.search;
-		history.replaceState('', document.title, url);
-	}
-
-	globalizeComponents(sections);
-
-	ReactDOM.render(
-		<StyleGuide
-			codeRevision={codeRevision}
-			config={styleguide.config}
-			slots={slots}
-			welcomeScreen={styleguide.welcomeScreen}
-			patterns={styleguide.patterns}
-			sections={sections}
-			displayMode={displayMode}
-		/>,
-		document.getElementById('app')
-	);
-}
+	ReactDOM.render(renderStyleguide(styleguide, codeRevision), document.getElementById('app'));
+};
 
 window.addEventListener('hashchange', renderStyleguide);
 
@@ -53,8 +20,8 @@ window.addEventListener('hashchange', renderStyleguide);
 if (module.hot) {
 	module.hot.accept('!!../loaders/styleguide-loader!./index.js', () => {
 		codeRevision += 1;
-		renderStyleguide();
+		render();
 	});
 }
 
-renderStyleguide();
+render();
diff --git a/src/rsg-components/Editor/Editor.js b/src/rsg-components/Editor/Editor.js
index eaab551e..b285c536 100644
--- a/src/rsg-components/Editor/Editor.js
+++ b/src/rsg-components/Editor/Editor.js
@@ -11,15 +11,6 @@ require('!!../../../loaders/style-loader!../../../loaders/css-loader!codemirror/
 // eslint-disable-next-line import/no-unresolved
 require('!!../../../loaders/style-loader!../../../loaders/css-loader!rsg-codemirror-theme.css');
 
-const codemirrorOptions = {
-	mode: 'jsx',
-	lineNumbers: false,
-	lineWrapping: true,
-	smartIndent: false,
-	matchBrackets: true,
-	viewportMargin: Infinity,
-};
-
 const UPDATE_DELAY = 10;
 
 export default class Editor extends Component {
@@ -46,11 +37,7 @@ export default class Editor extends Component {
 
 	render() {
 		const { code } = this.props;
-		const { highlightTheme } = this.context.config;
-		const options = {
-			...codemirrorOptions,
-			theme: highlightTheme,
-		};
-		return <CodeMirror value={code} onChange={this.handleChange} options={options} />;
+		const { editorConfig } = this.context.config;
+		return <CodeMirror value={code} onChange={this.handleChange} options={editorConfig} />;
 	}
 }
diff --git a/src/rsg-components/Editor/Editor.spec.js b/src/rsg-components/Editor/Editor.spec.js
index ccd7774f..2db90255 100644
--- a/src/rsg-components/Editor/Editor.spec.js
+++ b/src/rsg-components/Editor/Editor.spec.js
@@ -10,6 +10,9 @@ const options = {
 		config: {
 			showCode: false,
 			highlightTheme: 'base16-light',
+			editorConfig: {
+				mode: 'js',
+			},
 		},
 	},
 	childContextTypes: {
diff --git a/src/rsg-components/Editor/__snapshots__/Editor.spec.js.snap b/src/rsg-components/Editor/__snapshots__/Editor.spec.js.snap
index 63c2e3e1..9581eb34 100644
--- a/src/rsg-components/Editor/__snapshots__/Editor.spec.js.snap
+++ b/src/rsg-components/Editor/__snapshots__/Editor.spec.js.snap
@@ -5,13 +5,7 @@ exports[`Editor should renderer and editor 1`] = `
   onChange={[Function]}
   options={
     Object {
-      "lineNumbers": false,
-      "lineWrapping": true,
-      "matchBrackets": true,
-      "mode": "jsx",
-      "smartIndent": false,
-      "theme": "base16-light",
-      "viewportMargin": Infinity,
+      "mode": "js",
     }
   }
   value=
diff --git a/src/utils/getRouteData.js b/src/utils/getRouteData.js
index e70d8539..179141ab 100644
--- a/src/utils/getRouteData.js
+++ b/src/utils/getRouteData.js
@@ -4,7 +4,6 @@ import filterComponentsInSectionsByExactName from './filterComponentsInSectionsB
 import filterSectionExamples from './filterSectionExamples';
 import findSection from './findSection';
 import getInfoFromHash from './getInfoFromHash';
-import processSections from './processSections';
 import { DisplayModes } from '../consts';
 
 /**
@@ -14,11 +13,11 @@ import { DisplayModes } from '../consts';
  * #!/Button: show only Button section or Button component
  * #!/Button/1: show only the second example (index 1) of Button component
  *
- * @param {object} allSections
+ * @param {object} sections
  * @param {string} hash
  * @returns {object}
  */
-export default function getRouteData(allSections, hash) {
+export default function getRouteData(sections, hash) {
 	// Parse URL hash to check if the components list must be filtered
 	const {
 		// Name of the filtered component/section to show isolated (/#!/Button → Button)
@@ -29,8 +28,6 @@ export default function getRouteData(allSections, hash) {
 
 	let displayMode = DisplayModes.all;
 
-	let sections = processSections(allSections);
-
 	// Filter the requested component if required
 	if (targetName) {
 		const filteredComponents = filterComponentsInSectionsByExactName(sections, targetName);
diff --git a/src/utils/renderStyleguide.js b/src/utils/renderStyleguide.js
new file mode 100644
index 00000000..f9f67494
--- /dev/null
+++ b/src/utils/renderStyleguide.js
@@ -0,0 +1,53 @@
+import React from 'react';
+import slots from 'rsg-components/slots';
+import StyleGuide from 'rsg-components/StyleGuide';
+import getPageTitle from './getPageTitle';
+import getRouteData from './getRouteData';
+import globalizeComponents from './globalizeComponents';
+import processSections from './processSections';
+
+/**
+ * @param {object} styleguide An object returned by styleguide-loader
+ * @param {number} codeRevision
+ * @param {Location} [loc]
+ * @param {Document} [doc]
+ * @param {History} [hist]
+ * @return {React.ReactElement}
+ */
+export default function renderStyleguide(
+	styleguide,
+	codeRevision,
+	loc = window.location,
+	doc = document,
+	hist = window.history
+) {
+	const allSections = processSections(styleguide.sections);
+
+	// Globalize all components, not just ones we see on the screen, to make
+	// all components accessible to all examples
+	globalizeComponents(allSections);
+
+	const { sections, displayMode } = getRouteData(allSections, loc.hash);
+
+	// Update page title
+	doc.title = getPageTitle(sections, styleguide.config.title, displayMode);
+
+	// If the current hash location was set to just `/` (e.g. when navigating back from isolated view to overview)
+	// replace the URL with one without hash, to present the user with a single address of the overview screen
+	if (loc.hash === '#/') {
+		const url = loc.pathname + loc.search;
+		hist.replaceState('', doc.title, url);
+	}
+
+	return (
+		<StyleGuide
+			codeRevision={codeRevision}
+			config={styleguide.config}
+			slots={slots}
+			welcomeScreen={styleguide.welcomeScreen}
+			patterns={styleguide.patterns}
+			sections={sections}
+			displayMode={displayMode}
+		/>
+	);
+}


Test patch:
diff --git a/scripts/__tests__/config.spec.js b/scripts/__tests__/config.spec.js
index 1b8b6025..f2b2291d 100644
--- a/scripts/__tests__/config.spec.js
+++ b/scripts/__tests__/config.spec.js
@@ -197,3 +197,20 @@ it('should allow no webpack config', () => {
 	const fn = () => getConfig();
 	expect(fn).not.toThrow();
 });
+
+it('editorConfig option should have default values', () => {
+	const result = getConfig();
+	expect(result.editorConfig).toHaveProperty('mode', 'jsx');
+	expect(result.editorConfig).toHaveProperty('theme', 'base16-light');
+});
+
+it('should merge default editorConfig with options provided by the user', () => {
+	const result = getConfig({
+		editorConfig: {
+			mode: 'js',
+		},
+	});
+
+	expect(result.editorConfig).toHaveProperty('mode', 'js');
+	expect(result.editorConfig).toHaveProperty('theme', 'base16-light');
+});
diff --git a/src/utils/__tests__/__snapshots__/renderStyleguide.spec.js.snap b/src/utils/__tests__/__snapshots__/renderStyleguide.spec.js.snap
new file mode 100644
index 00000000..d0ead0d4
--- /dev/null
+++ b/src/utils/__tests__/__snapshots__/renderStyleguide.spec.js.snap
@@ -0,0 +1,66 @@
+// Jest Snapshot v1, https://goo.gl/fbAQLP
+
+exports[`renderStyleguide should render StyleGuide component 1`] = `
+<Styled(StyleGuide)
+  homepageUrl="https://react-styleguidist.js.org/"
+  title="My Style Guide"
+  toc={
+    <TableOfContents
+      sections={
+        Array [
+          Object {
+            "components": Array [
+              Object {
+                "module": "ButtonModule",
+                "name": "Button",
+                "props": Object {
+                  "displayName": "Button",
+                  "examples": Array [],
+                },
+              },
+              Object {
+                "module": "ImageModule",
+                "name": "Image",
+                "props": Object {
+                  "displayName": "Image",
+                  "examples": Array [],
+                },
+              },
+            ],
+            "sections": Array [],
+          },
+        ]
+      }
+    />
+  }
+>
+  <Sections
+    depth={1}
+    sections={
+      Array [
+        Object {
+          "components": Array [
+            Object {
+              "module": "ButtonModule",
+              "name": "Button",
+              "props": Object {
+                "displayName": "Button",
+                "examples": Array [],
+              },
+            },
+            Object {
+              "module": "ImageModule",
+              "name": "Image",
+              "props": Object {
+                "displayName": "Image",
+                "examples": Array [],
+              },
+            },
+          ],
+          "sections": Array [],
+        },
+      ]
+    }
+  />
+</Styled(StyleGuide)>
+`;
diff --git a/src/utils/__tests__/getRouteData.spec.js b/src/utils/__tests__/getRouteData.spec.js
index 00b504b5..b7fce1d7 100644
--- a/src/utils/__tests__/getRouteData.spec.js
+++ b/src/utils/__tests__/getRouteData.spec.js
@@ -8,6 +8,7 @@ const sections = deepfreeze([
 			{
 				components: [
 					{
+						name: 'Button',
 						props: {
 							displayName: 'Button',
 							examples: ['example 0', 'example 1'],
@@ -15,16 +16,20 @@ const sections = deepfreeze([
 						module: 1,
 					},
 					{
+						name: 'Image',
 						props: {
 							displayName: 'Image',
 						},
 						module: 2,
 					},
 				],
+				sections: [],
 			},
 			{
 				name: 'Section',
 				content: ['example 0', 'example 1'],
+				components: [],
+				sections: [],
 			},
 		],
 	},
diff --git a/src/utils/__tests__/globalizeComponent.spec.js b/src/utils/__tests__/globalizeComponent.spec.js
index b378f740..78fb384a 100644
--- a/src/utils/__tests__/globalizeComponent.spec.js
+++ b/src/utils/__tests__/globalizeComponent.spec.js
@@ -1,5 +1,9 @@
 import globalizeComponent from '../globalizeComponent';
 
+afterEach(() => {
+	delete global.Foo;
+});
+
 describe('globalizeComponent', () => {
 	it('should set component’s module as a global variable', () => {
 		const globalsCount = Object.keys(global).length;
diff --git a/src/utils/__tests__/globalizeComponents.spec.js b/src/utils/__tests__/globalizeComponents.spec.js
index 8f0f4e1a..fe8269a8 100644
--- a/src/utils/__tests__/globalizeComponents.spec.js
+++ b/src/utils/__tests__/globalizeComponents.spec.js
@@ -1,5 +1,10 @@
 import globalizeComponents from '../globalizeComponents';
 
+afterEach(() => {
+	delete global.Foo;
+	delete global.Bar;
+});
+
 describe('globalizeComponents', () => {
 	it('should set all components’ modules as a global variables', () => {
 		const globalsCount = Object.keys(global).length;
diff --git a/src/utils/__tests__/renderStyleguide.spec.js b/src/utils/__tests__/renderStyleguide.spec.js
new file mode 100644
index 00000000..3ed7dfba
--- /dev/null
+++ b/src/utils/__tests__/renderStyleguide.spec.js
@@ -0,0 +1,88 @@
+import renderStyleguide from '../renderStyleguide';
+
+const styleguide = {
+	config: {
+		title: 'My Style Guide',
+	},
+	sections: [
+		{
+			components: [
+				{
+					props: {
+						displayName: 'Button',
+					},
+					module: 'ButtonModule',
+				},
+				{
+					props: {
+						displayName: 'Image',
+					},
+					module: 'ImageModule',
+				},
+			],
+		},
+	],
+	welcomeScreen: false,
+	patterns: ['button', 'input'],
+};
+const codeRevision = 1;
+const location = {
+	hash: '',
+};
+const doc = {
+	title: () => {},
+};
+const history = {
+	replaceState: () => {},
+};
+
+afterEach(() => {
+	delete global.Button;
+	delete global.Image;
+});
+
+describe('renderStyleguide', () => {
+	it('should render StyleGuide component', () => {
+		const result = shallow(renderStyleguide(styleguide, codeRevision, location, doc, history));
+		expect(result).toMatchSnapshot();
+	});
+
+	it('should globalize all components', () => {
+		renderStyleguide(styleguide, codeRevision, location, doc, history);
+		expect(global.Button).toBe('ButtonModule');
+		expect(global.Image).toBe('ImageModule');
+	});
+
+	it('should globalize all components in isolated mode', () => {
+		renderStyleguide(styleguide, codeRevision, { hash: '#!/Button' }, doc, history);
+		expect(global.Button).toBe('ButtonModule');
+		expect(global.Image).toBe('ImageModule');
+	});
+
+	it('should change document title', () => {
+		const title = jest.fn();
+		const location = { hash: '' };
+		Object.defineProperty(location, 'title', {
+			set: title,
+		});
+		renderStyleguide(styleguide, codeRevision, location, location, history);
+		expect(title).toBeCalledWith('My Style Guide');
+	});
+
+	it('should change document title in isolated mode', () => {
+		const title = jest.fn();
+		const location = { hash: '#!/Button' };
+		Object.defineProperty(location, 'title', {
+			set: title,
+		});
+		renderStyleguide(styleguide, codeRevision, location, location, history);
+		expect(title).toBeCalledWith('Button — My Style Guide');
+	});
+
+	it('should remove #/ from the address bar', () => {
+		const location = { hash: '#/', pathname: '/pizza', search: '?foo=bar' };
+		const history = { replaceState: jest.fn() };
+		renderStyleguide(styleguide, codeRevision, location, location, history);
+		expect(history.replaceState).toBeCalledWith('', 'My Style Guide', '/pizza?foo=bar');
+	});
+});