diff --git a/.eslintignore b/.eslintignore index 84cf66469e19d..164ae5ac2c7ef 100644 --- a/.eslintignore +++ b/.eslintignore @@ -1,4 +1,4 @@ build coverage -docs vendor +node_modules diff --git a/.eslintrc.json b/.eslintrc.json index 76fc971b5fea4..a1e9739eda4b4 100644 --- a/.eslintrc.json +++ b/.eslintrc.json @@ -6,14 +6,14 @@ "plugin:react/recommended", "plugin:jsx-a11y/recommended", "plugin:jest/recommended" - ], + ], "env": { "browser": false, "es6": true, "node": true, "mocha": true, "jest/globals": true - }, + }, "parserOptions": { "sourceType": "module", "ecmaFeatures": { diff --git a/.gitignore b/.gitignore index 69f1023725d88..11169828c8085 100644 --- a/.gitignore +++ b/.gitignore @@ -2,7 +2,6 @@ build coverage node_modules -storybook-static gutenberg.zip # Directories/files that may appear in your environment diff --git a/.storybook/addons.js b/.storybook/addons.js deleted file mode 100644 index 5659100510d17..0000000000000 --- a/.storybook/addons.js +++ /dev/null @@ -1,2 +0,0 @@ -import '@storybook/addon-knobs/register'; -import '@storybook/addon-options/register'; diff --git a/.storybook/config.js b/.storybook/config.js deleted file mode 100644 index 35d9b93b2b298..0000000000000 --- a/.storybook/config.js +++ /dev/null @@ -1,43 +0,0 @@ -/** - * External dependencies - */ -import 'prismjs'; -import { configure, setAddon } from '@storybook/react'; -import infoAddon from '@storybook/addon-info'; -import { setOptions } from '@storybook/addon-options'; - -/** - * Internal dependencies - */ -import * as element from 'element'; -import './style.scss'; - -function loadStories() { - window.wp = { ...window.wp, element }; - require( './stories/intro' ); - require( './stories/contributing' ); - require( './stories/coding-guidelines' ); - require( './stories/design' ); - require( '../i18n/story' ); - require( '../element/story' ); - require( '../blocks/story' ); - require( '../editor/story' ); - require( '../components/story' ); - require( '../components/button/story' ); - require( '../components/higher-order/story' ); - require( '../components/higher-order/with-instance-id/story' ); -} - -setOptions( { - name: 'Gutenberg', - url: 'https://github.com/WordPress/gutenberg', - goFullScreen: false, - showLeftPanel: true, - showDownPanel: true, - showSearchBox: false, - downPanelInRight: true, - sortStoriesByKind: false, -} ); -setAddon( infoAddon ); - -configure( loadStories, module ); diff --git a/.storybook/stories/coding-guidelines.js b/.storybook/stories/coding-guidelines.js deleted file mode 100644 index 85ec888d93f41..0000000000000 --- a/.storybook/stories/coding-guidelines.js +++ /dev/null @@ -1,15 +0,0 @@ -/** - * External dependencies - */ -import ReactMarkdown from 'react-markdown'; -import { storiesOf } from '@storybook/react'; -import { withKnobs } from '@storybook/addon-knobs'; - -/** - * Internal dependencies - */ -import readme from '../../docs/coding-guidelines.md'; - -storiesOf( 'Gutenberg', module ) - .addDecorator( withKnobs ) - .add( 'Coding Guidelines', () => ); diff --git a/.storybook/stories/contributing.js b/.storybook/stories/contributing.js deleted file mode 100644 index aa11a3a7a3ffc..0000000000000 --- a/.storybook/stories/contributing.js +++ /dev/null @@ -1,15 +0,0 @@ -/** - * External dependencies - */ -import ReactMarkdown from 'react-markdown'; -import { storiesOf } from '@storybook/react'; -import { withKnobs } from '@storybook/addon-knobs'; - -/** - * Internal dependencies - */ -import readme from '../../CONTRIBUTING.md'; - -storiesOf( 'Gutenberg', module ) - .addDecorator( withKnobs ) - .add( 'Contributing', () => ); diff --git a/.storybook/stories/design.js b/.storybook/stories/design.js deleted file mode 100644 index c2224d84af8c3..0000000000000 --- a/.storybook/stories/design.js +++ /dev/null @@ -1,15 +0,0 @@ -/** - * External dependencies - */ -import ReactMarkdown from 'react-markdown'; -import { storiesOf } from '@storybook/react'; -import { withKnobs } from '@storybook/addon-knobs'; - -/** - * Internal dependencies - */ -import readme from '../../docs/design.md'; - -storiesOf( 'Gutenberg', module ) - .addDecorator( withKnobs ) - .add( 'Design', () => ); diff --git a/.storybook/stories/intro.js b/.storybook/stories/intro.js deleted file mode 100644 index 6798f125317c8..0000000000000 --- a/.storybook/stories/intro.js +++ /dev/null @@ -1,15 +0,0 @@ -/** - * External dependencies - */ -import ReactMarkdown from 'react-markdown'; -import { storiesOf } from '@storybook/react'; -import { withKnobs } from '@storybook/addon-knobs'; - -/** - * Internal dependencies - */ -import readme from '../../README.md'; - -storiesOf( 'Gutenberg', module ) - .addDecorator( withKnobs ) - .add( 'Intro', () => ); diff --git a/.storybook/style.scss b/.storybook/style.scss deleted file mode 100644 index 59e6f6366a2c5..0000000000000 --- a/.storybook/style.scss +++ /dev/null @@ -1,52 +0,0 @@ -@import url( 'https://wordpress.org/wp-admin/load-styles.php?c=0&dir=ltr&load%5B%5D=common,buttons,dashicons,forms' ); - -html { - height: auto; -} - -body { - background: none; - padding: 20px; - margin: 0; -} - -p, h1, h2, li { - // Match Info Addon Styling - font-family: -apple-system, ".SFNSText-Regular", "San Francisco", Roboto, "Segoe UI", "Helvetica Neue", "Lucida Grande", sans-serif; - color: rgb(68, 68, 68); - -webkit-font-smoothing: antialiased; - font-weight: 400; - line-height: 1.45; - font-size: 15px; -} - -h1 { - font-size: 2em; - margin: .67em 0; - font-weight: 600; -} - -h2 { - margin: 0px 0px 10px; - padding: 0px; - font-weight: 400; - font-size: 22px; -} - -pre { - font-size: 0.88em; - font-family: Menlo, Monaco, "Courier New", monospace; - background-color: rgb(250, 250, 250); - padding: 0.5rem; - line-height: 1.5; - overflow-x: auto !important; // Overrides the Info Addon Styling - - &:before, &:after { - background: red; - } -} - -code { - font-family: Menlo, Monaco, "Courier New", monospace; - background-color: rgb(250, 250, 250); -} diff --git a/.storybook/webpack.config.js b/.storybook/webpack.config.js deleted file mode 100644 index e24e1bd60eb89..0000000000000 --- a/.storybook/webpack.config.js +++ /dev/null @@ -1,35 +0,0 @@ -const config = require( '../webpack.config' ); -const webpack = require( 'webpack' ); -config.module.rules = [ - // Exclude the sass loader to override it - ...config.module.rules.filter( ( rule ) => ! rule.test.test( '.scss' ) ), - { - test: /\.md/, - use: 'raw-loader', - }, - { - test: /\.scss$/, - use: [ - { loader: 'style-loader' }, - { loader: 'css-loader' }, - { loader: 'postcss-loader' }, - { - loader: 'sass-loader', - query: { - includePaths: [ 'editor/assets/stylesheets' ], - data: '@import "variables"; @import "mixins"; @import "animations";@import "z-index";', - outputStyle: 'production' === process.env.NODE_ENV ? - 'compressed' : 'nested', - }, - }, - ], - }, -]; -config.externals = []; - -// Exclude Uglify Plugin to avoid breaking the React Components Display Name -config.plugins = config.plugins.filter( plugin => { - return plugin.constructor !== webpack.optimize.UglifyJsPlugin; -} ); - -module.exports = config; diff --git a/.travis.yml b/.travis.yml index 4e162f598ffcd..13199ddfc7b7a 100644 --- a/.travis.yml +++ b/.travis.yml @@ -80,10 +80,16 @@ script: fi before_deploy: - - npm install && npm run build-storybook + - npm install + - cd docutron + - npm install + - cd ../ + - npm run docs-build deploy: - provider: surge - project: ./storybook-static/ - domain: gutenberg-devdoc.surge.sh + provider: pages + local_dir: ./docutron/build/ skip_cleanup: true + github_token: $GITHUB_TOKEN + on: + branch: master diff --git a/_config.yml b/_config.yml deleted file mode 100644 index c4192631f25b3..0000000000000 --- a/_config.yml +++ /dev/null @@ -1 +0,0 @@ -theme: jekyll-theme-cayman \ No newline at end of file diff --git a/blocks/story/index.js b/blocks/story/index.js deleted file mode 100644 index ff05da78b4323..0000000000000 --- a/blocks/story/index.js +++ /dev/null @@ -1,15 +0,0 @@ -/** - * External dependencies - */ -import ReactMarkdown from 'react-markdown'; -import { storiesOf } from '@storybook/react'; -import { withKnobs } from '@storybook/addon-knobs'; - -/** - * Internal dependencies - */ -import readme from '../README.md'; - -storiesOf( 'Modules', module ) - .addDecorator( withKnobs ) - .add( 'Blocks', () => ); diff --git a/components/button/story/index.js b/components/button/story/index.js deleted file mode 100644 index 5e05caf4bc3a8..0000000000000 --- a/components/button/story/index.js +++ /dev/null @@ -1,44 +0,0 @@ -/** - * External dependencies - */ -import ReactMarkdown from 'react-markdown'; -import { storiesOf } from '@storybook/react'; -import { text, boolean, withKnobs } from '@storybook/addon-knobs'; - -/** - * Internal dependencies - */ -import Button from '../'; -import readme from '../README.md'; - -const decorateWordPressUI = ( story ) => ( -
- { story() } -
-); - -const defaultInfoConfig = { - header: true, - inline: true, - propTables: false, - styles: ( style ) => ( { ...style, header: { ...style.header, h1: { display: 'none' } } } ), -}; - -storiesOf( 'Components', module ) - .addDecorator( decorateWordPressUI ) - .addDecorator( withKnobs ) - .addWithInfo( - 'Button', - ( ), - () => ( - - ), - defaultInfoConfig - ); diff --git a/components/higher-order/story/index.js b/components/higher-order/story/index.js deleted file mode 100644 index f16f59add5f3a..0000000000000 --- a/components/higher-order/story/index.js +++ /dev/null @@ -1,15 +0,0 @@ -/** - * External dependencies - */ -import ReactMarkdown from 'react-markdown'; -import { storiesOf } from '@storybook/react'; -import { withKnobs } from '@storybook/addon-knobs'; - -/** - * Internal dependencies - */ -import readme from '../README.md'; - -storiesOf( 'Higher Order Components', module ) - .addDecorator( withKnobs ) - .add( 'Intro', () => ); diff --git a/components/higher-order/with-instance-id/story/index.js b/components/higher-order/with-instance-id/story/index.js deleted file mode 100644 index a2f498b303395..0000000000000 --- a/components/higher-order/with-instance-id/story/index.js +++ /dev/null @@ -1,15 +0,0 @@ -/** - * External dependencies - */ -import ReactMarkdown from 'react-markdown'; -import { storiesOf } from '@storybook/react'; -import { withKnobs } from '@storybook/addon-knobs'; - -/** - * Internal dependencies - */ -import readme from '../README.md'; - -storiesOf( 'Higher Order Components', module ) - .addDecorator( withKnobs ) - .add( 'withInstanceId', () => ); diff --git a/components/story/index.js b/components/story/index.js deleted file mode 100644 index bfc34c003f743..0000000000000 --- a/components/story/index.js +++ /dev/null @@ -1,15 +0,0 @@ -/** - * External dependencies - */ -import ReactMarkdown from 'react-markdown'; -import { storiesOf } from '@storybook/react'; -import { withKnobs } from '@storybook/addon-knobs'; - -/** - * Internal dependencies - */ -import readme from '../README.md'; - -storiesOf( 'Components', module ) - .addDecorator( withKnobs ) - .add( 'Intro', () => ); diff --git a/docs/attribute-matchers.md b/docs/attribute-matchers.md new file mode 100644 index 0000000000000..69668484b7c60 --- /dev/null +++ b/docs/attribute-matchers.md @@ -0,0 +1,62 @@ +# Attribute Matchers + +Attribute matchers are used to define the strategy by which block attribute values are extracted from saved post content. They provide a mechanism to map from the saved markup to a JavaScript representation of a block. + +Each matcher accepts an optional selector as the first argument. If a selector is specified, the matcher behavior will be run against the corresponding element(s) contained within the block. Otherwise it will be run against the block's root node. + +Under the hood, attribute matchers are a superset of functionality provided by [hpq](https://github.com/aduth/hpq), a small library used to parse and query HTML markup into an object shape. In an object of attributes matchers, you can name the keys as you see fit. The resulting object will assign as a value to each key the result of its attribute matcher. + +## Common Matchers + +### `attr` + +Use `attr` to extract the value of an attribute from markup. + +_Example_: Extract the `src` attribute from an image found in the block's markup. + +```js +{ + url: attr( 'img', 'src' ) +} +// { "url": "https://lorempixel.com/1200/800/" } +``` + +### `children` + +Use `children` to extract child nodes of the matched element, returned as an array of virtual elements. This is most commonly used in combination with the `Editable` component. + +_Example_: Extract child nodes from a paragraph of rich text. + +```js +{ + content: children( 'p' ) +} +// { +// "content": [ +// "Vestibulum eu ", +// { "type": "strong", "children": "tortor" }, +// " vel urna." +// ] +// } +``` + +### `query` + +Use `query` to extract an array of values from markup. Entries of the array are determined by the selector argument, where each matched element within the block will have an entry structured corresponding to the second argument, an object of attribute matchers. + +_Example_: Extract `src` and `alt` from each image element in the block's markup. + +```js +{ + images: query( 'img', { + url: attr( 'src' ) + alt: attr( 'alt' ) + } ) +} +// { +// "images": [ +// { "url": "https://lorempixel.com/1200/800/", "alt": "large image" }, +// { "url": "https://lorempixel.com/50/50/", "alt": "small image" } +// ] +// } +``` diff --git a/docs/blocks-basic.md b/docs/blocks-basic.md new file mode 100644 index 0000000000000..9afe9cacac5e7 --- /dev/null +++ b/docs/blocks-basic.md @@ -0,0 +1,83 @@ +# Writing Your First Block Type + +To keep things simple for our first example, let's create a new block type which displays a styled message in a post. At this point, we won't allow the user to edit the message. We'll learn more about editable fields in later sections. + +Blocks containing static content are implemented entirely in JavaScript using the `registerBlockType` function. This function is responsible for specifying the blueprint of a block, describing the behaviors necessary for the editor to understand how it appears, changes when edited, and is ultimately saved in the post's content. + +## Enqueueing Block Scripts + +While the block type itself is implemented in JavaScript, you'll need to use the `enqueue_block_editor_assets` [WordPress action](https://codex.wordpress.org/Glossary#Action) to have your scripts included in the editor. This is similar to the [`wp_enqueue_scripts` action](https://developer.wordpress.org/reference/hooks/wp_enqueue_scripts/), but specifically targeting editor scripts and styles. + +```php +Hello editor.

; + }, + + save() { + return

Hello saved content.

; + }, +} ); +``` +{% end %} + +Once a block is registered, you should immediately see that it becomes available as an option in the editor inserter dialog, using values from `title`, `icon`, and `category` to organize its display. You can choose an icon from any included in the built-in [Dashicons icon set](https://developer.wordpress.org/resource/dashicons/). + +A block name must be prefixed with a namespace specific to your plugin. This helps prevent conflicts when more than one plugin registers a block with the same name. + +The `edit` and `save` functions describe the structure of your block in the context of the editor and the saved content respectively. While the difference is not obvious in this simple example, in the following sections we'll explore how these are used to enable customization of the block in the editor preview. diff --git a/docs/blocks-controls.md b/docs/blocks-controls.md new file mode 100644 index 0000000000000..bb5699324c579 --- /dev/null +++ b/docs/blocks-controls.md @@ -0,0 +1,152 @@ +# Block Controls: Toolbars and Inspector + +To simplify block customization and ensure a consistent experience for users, there are a number of built-in UI patterns to help generate the editor preview. Like with the `Editable` component covered in the previous chapter, the `wp.blocks` global includes a few other common components to render editing interfaces. In this chapter, we'll explore toolbars and the block inspector. + +## Toolbar + +toolbar + +When the user selects a block, a number of control buttons may be shown in a toolbar above the selected block. Some of these block-level controls are included automatically if the editor is able to transform the block to another type, or if the focused element is an Editable component. + +You can also customize the toolbar to include controls specific to your block type. If the return value of your block type's `edit` function includes a `BlockControls` element, those controls will be shown in the selected block's toolbar. + +{% codetabs %} +{% ES5 %} +```js +var el = wp.element.createElement, + registerBlockType = wp.blocks.registerBlockType, + Editable = wp.blocks.Editable, + BlockControls = wp.blocks.BlockControls, + AlignmentToolbar = wp.blocks.AlignmentToolbar, + children = wp.blocks.query.children; + +registerBlockType( 'gutenberg-boilerplate-es5/hello-world-step-04', { + title: 'Hello World (Step 4)', + + icon: 'universal-access-alt', + + category: 'layout', + + attributes: { + content: children( 'p' ), + }, + + edit: function( props ) { + var content = props.attributes.content, + alignment = props.attributes.alignment, + focus = props.focus; + + function onChangeContent( newContent ) { + props.setAttributes( { content: newContent } ); + } + + function onChangeAlignment( newAlignment ) { + props.setAttributes( { alignment: newAlignment } ); + } + + return [ + !! focus && el( + BlockControls, + { key: 'controls' }, + el( + AlignmentToolbar, + { + value: alignment, + onChange: onChangeAlignment + } + ) + ), + el( + Editable, + { + key: 'editable', + tagName: 'p', + className: props.className, + style: { textAlign: alignment }, + onChange: onChangeContent, + value: content, + focus: focus, + onFocus: props.setFocus + } + ) + ]; + }, + + save: function( props ) { + var content = props.attributes.content; + + return el( 'p', { className: props.className }, content ); + }, +} ); +``` +{% ESNext %} +```js +const { + registerBlockType, + Editable, + BlockControls, + AlignmentToolbar, + query +} = wp.blocks; +const { children } = children; + +registerBlockType( 'gutenberg-boilerplate-esnext/hello-world-step-04', { + title: 'Hello World (Step 4)', + + icon: 'universal-access-alt', + + category: 'layout', + + attributes: { + content: children( 'p' ), + }, + + edit( { attributes, setAttributes, focus } ) { + const { content, alignment } = attributes; + + function onChangeContent( newContent ) { + setAttributes( { content: newContent } ); + } + + function onChangeAlignment( newAlignment ) { + setAttributes( { alignment: newAlignment } ); + } + + return [ + !! focus && ( + + + + ), + + ]; + }, + + save( { attributes, className } ) { + const { content } = attributes; + + return

{ content }

; + }, +} ); +``` +{% end %} + +Note that you should only include `BlockControls` if the block is currently selected. We must test that the `focus` value is truthy before rendering the element, otherwise you will inadvertently cause controls to be shown for the incorrect block type. + +## Inspector + +While the toolbar area is useful for displaying controls to toggle attributes of a block, sometimes you will find that you need more screen space for form fields. The inspector region is shown in place of the post settings sidebar when a block is selected. + +Similar to rendering a toolbar, if you include an `InspectorControls` element in the return value of your block type's `edit` function, those controls will be shown in the inspector region. diff --git a/docs/blocks-editable.md b/docs/blocks-editable.md new file mode 100644 index 0000000000000..c0b40f2a29076 --- /dev/null +++ b/docs/blocks-editable.md @@ -0,0 +1,114 @@ +# Introducing Attributes and Editable Fields + +Our example block is still not very interesting because it lacks options to customize the appearance of the message. In this section, we will implement an editable field allowing the user to specify their own message. Before doing so, it's important to understand how the state of a block (its "attributes") is maintained and changed over time. + +## Attributes + +Until now, the `edit` and `save` functions have returned a simple representation of a paragraph element. We also learned how these functions are responsible for _describing_ the structure of the block's appearance. If the user changes a block, this structure may need to change. To achieve this, the state of a block is maintained throughout the editing session as a plain JavaScript object, and when an update occurs, the `edit` function is invoked again. Put another way: __the output of a block is a function of its attributes__. + +One challenge of maintaining the representation of a block as a JavaScript object is that we must be able to extract this object again from the saved content of a post. This is achieved with the block type's `attribute` property: + +{% codetabs %} +{% ES5 %} +```js +var el = wp.element.createElement, + registerBlockType = wp.blocks.registerBlockType, + Editable = wp.blocks.Editable, + children = wp.blocks.query.children; + +registerBlockType( 'gutenberg-boilerplate-es5/hello-world-step-03', { + title: 'Hello World (Step 3)', + + icon: 'universal-access-alt', + + category: 'layout', + + attributes: { + content: children( 'p' ), + }, + + edit: function( props ) { + var content = props.attributes.content, + focus = props.focus; + + function onChangeContent( newContent ) { + props.setAttributes( { content: newContent } ); + } + + return el( + Editable, + { + tagName: 'p', + className: props.className, + onChange: onChangeContent, + value: content, + focus: focus, + onFocus: props.setFocus + } + ); + }, + + save: function( props ) { + var content = props.attributes.content; + + return el( 'p', { className: props.className }, content ); + }, +} ); +``` +{% ESNext %} +```js +const { registerBlockType, Editable, query } = wp.blocks; +const { children } = query; + +registerBlockType( 'gutenberg-boilerplate-esnext/hello-world-step-03', { + title: 'Hello World (Step 3)', + + icon: 'universal-access-alt', + + category: 'layout', + + attributes: { + content: children( 'p' ), + }, + + edit( { attributes, setAttributes, focus, className } ) { + const { content } = attributes; + + function onChangeContent( newContent ) { + setAttributes( { content: newContent } ); + } + + return ( + + ); + }, + + save( { attributes, className } ) { + const { content } = attributes; + + return

{ content }

; + }, +} ); +``` +{% end %} + +When registering a new block type, the `attributes` property describes the shape of the attributes object you'd like to receive in the `edit` and `save` functions. Each value is a [matcher function]() to find the desired value from the markup of the block. + +In the code snippet above, when loading the editor, we will extract the `content` value as the children of the paragraph element in the saved post's markup. + +## Components and the `Editable` Component + +Earlier examples used the `createElement` function to create DOM nodes, but it's also possible to encapsulate this behavior into ["components"](). This abstraction helps as a pattern to share common behaviors and to hide complexity into self-contained units. There are a number of components available to use in implementing your blocks. You can see one such component in the snippet above: the [`Editable` component](). + +The `Editable` component can be considered as a super-powered `textarea` element, enabling rich content editing including bold, italics, hyperlinks, etc. It is not too much unlike the single editor region of the legacy post editor, and is in fact powered by the same TinyMCE library. + +Implementing this behavior as a component enables you as the block implementer to be much more granular about editable fields. Your block may not need `Editable` at all, or it may need many independent `Editable` elements, each operating on a subset of the overall block state. + +Because `Editable` allows for nested nodes, you'll most often use it in conjunction with the `children` attribute matcher when extracting the value from saved content. diff --git a/docs/blocks-stylesheet.md b/docs/blocks-stylesheet.md new file mode 100644 index 0000000000000..9fc07fff8ba11 --- /dev/null +++ b/docs/blocks-stylesheet.md @@ -0,0 +1,105 @@ +# Applying Styles From a Stylesheet + +In the previous section, the block had applied its own styles by an inline `style` attribute. While this might be adequate for very simple components, you will quickly find that it becomes easier to write your styles by extracting them to a separate stylesheet file. + +The editor will automatically generate a class name for each block type to simplify styling. It can be accessed from the object argument passed to the edit and save functions: + +{% codetabs %} +{% ES5 %} +```js +var el = wp.element.createElement, + registerBlockType = wp.blocks.registerBlockType; + +registerBlockType( 'gutenberg-boilerplate-es5/hello-world-step-02', { + title: 'Hello World (Step 2)', + + icon: 'universal-access-alt', + + category: 'layout', + + edit: function( props ) { + return el( 'p', { className: props.className }, 'Hello editor.' ); + }, + + save: function( props ) { + return el( 'p', { className: props.className }, 'Hello saved content.' ); + }, +} ); +``` +{% ESNext %} +```js +const { registerBlockType } = wp.blocks; + +registerBlockType( 'gutenberg-boilerplate-esnext/hello-world-step-02', { + title: 'Hello World (Step 2)', + + icon: 'universal-access-alt', + + category: 'layout', + + edit( { className } ) { + return

Hello editor.

; + }, + + save( { className } ) { + return

Hello saved content.

; + }, +} ); +``` +{% end %} + +The class name is generated using the block's name prefixed with `wp-block-`, replacing the `/` namespace separator with a single `-`. + +```css +.wp-block-gutenberg-boilerplate-es5-hello-world-step-02 { + color: green; + background: #cfc; + border: 2px solid #9c9; + padding: 20px; +} +``` + +## Enqueuing Block Styles + +Like scripts, your block's editor-specific styles should be enqueued during the `enqueue_block_editor_assets` action. + +```php +kickoff post: +The all-encompassing goal of Gutenberg is to create a post and page building experience that makes it easy to create _rich post layouts_. From the [kickoff post](https://make.wordpress.org/core/2017/01/04/focus-tech-and-design-leads/): > The editor will endeavour to create a new page and post building experience that makes writing rich posts effortless, and has “blocks” to make it easy what today might take shortcodes, custom HTML, or “mystery meat” embed discovery. diff --git a/docs/faq.md b/docs/faq.md index 8b590470c348f..21c2115c243da 100644 --- a/docs/faq.md +++ b/docs/faq.md @@ -1,4 +1,4 @@ -# Gutenberg F.A.Q. +# Frequently Asked Questions ## What is Gutenberg? @@ -132,7 +132,7 @@ $blocks = $parser->parse( $post_content ); Blocks are likely to become the main way users interact with content. Users are going to be discovering functionality in the new universal inserter tool, with a richer block interface that provides more layout opportunities. ## What features will be available at launch? What does the post-launch roadmap look like? -As part of the focus on the editor in 2017, a focus on customization and sitebuilding is next. From the kickoff post: +As part of the focus on the editor in 2017, a focus on customization and sitebuilding is next. From [the kickoff post](https://make.wordpress.org/core/2017/01/04/focus-tech-and-design-leads/): > The customizer will help out the editor at first, then shift to bring those fundamental building blocks into something that could allow customization “outside of the box” of post_content, including sidebars and possibly even an entire theme. diff --git a/docs/glossary.md b/docs/glossary.md new file mode 100644 index 0000000000000..70a7fff8353ec --- /dev/null +++ b/docs/glossary.md @@ -0,0 +1,15 @@ +# Glossary + +- __Attribute matchers__: An object describing the attributes shape of a block. The keys can be named as most appropriate to describe the state of a block type. The value for each key is a function which describes the strategy by which the attribute value should be extracted from the content of a saved post's content. When processed, a new object is created, taking the form of the keys defined in the attribute matchers, where each value is the result of the attribute matcher function. +- __Attributes__: The object representation of the current state of a block in post content. When loading a saved post, this is determined by the attribute matchers for the block type. These values can change over time during an editing session when the user modifies a block, and are used when determining how to serialize the block. +- __Block__: The abstract term used to describe units of markup that, composed together, form the content or layout of a webpage. The idea combines concepts of what in WordPress today we achieve with shortcodes, custom HTML, and embed discovery into a single consistent API and user experience. +- __Block name__: A unique identifier for a block type, consisting of a plugin-specific namespace and a short label describing the block's intent. e.g. `core/image` +- __Block type__: In contrast with the blocks composing a particular post, a block type describes the blueprint by which any block of that type should behave. So while there may be many images within a post, each behaves consistent with a unified image block type definition. +- __Dynamic block__: A type of block where the content of which may change and cannot be determined at the time of saving a post, instead calculated any time the post is shown on the front of a site. These blocks may save fallback content or no content at all in their JavaScript implementation, instead deferring to a PHP block implementation for runtime rendering. +- __Editable__: A common component enabling rich content editing including bold, italics, hyperlinks, etc. It is not too much unlike the single editor region of the legacy post editor, and is in fact powered by the same TinyMCE library. +- __Inspector__: A block settings region shown in place of the post settings when a block is selected. Fields may be shown here to allow the user to customize the selected block. +- __Post settings__: A sidebar region containing metadata fields for the post, including scheduling, visibility, terms, and featured image. +- __Serialization__: The process of converting a block's attributes object into HTML markup, typically occurring when saving the post. +- __Static block__: A type of block where the content of which is known at the time of saving a post. A static block will be saved with HTML markup directly in post content. +- __TinyMCE__: [TinyMCE](https://www.tinymce.com/) is a web-based JavaScript WYSIWYG (What You See Is What You Get) editor. +- __Toolbar__: A set of button controls. In the context of a block, usually referring to the toolbar of block controls shown above the selected block. diff --git a/docs/index.js b/docs/index.js new file mode 100644 index 0000000000000..cff85abc0e16d --- /dev/null +++ b/docs/index.js @@ -0,0 +1,86 @@ +/** + * WordPress dependencies + */ +import { addStory } from 'docutron'; + +addStory( { + name: 'intro', + title: 'Introduction', + path: '/', + markdown: require( './readme.md' ), +} ); + +addStory( { + name: 'blocks', + title: 'Creating Block Types', + markdown: require( './blocks.md' ), +} ); + +addStory( { + parents: [ 'blocks' ], + name: 'writing-your-first-block-type', + title: 'Writing Your First Block Type', + markdown: require( 'blocks-basic.md' ), +} ); + +addStory( { + parents: [ 'blocks' ], + name: 'applying-styles-with-stylesheets', + title: 'Applying Styles With Stylesheets', + markdown: require( './blocks-stylesheet.md' ), +} ); + +addStory( { + parents: [ 'blocks' ], + name: 'introducing-attributes-and-editable-fields', + title: 'Introducing Attributes and Editable Fields', + markdown: require( './blocks-editable.md' ), +} ); + +addStory( { + parents: [ 'blocks' ], + name: 'block-controls-toolbars-and-inspector', + title: 'Block Controls: Toolbars and Inspector', + markdown: require( './blocks-controls.md' ), +} ); + +addStory( { + name: 'reference', + title: 'Reference', + markdown: require( './reference.md' ), +} ); + +addStory( { + parents: [ 'reference' ], + name: 'attribute-matchers', + title: 'Attribute Matchers', + markdown: require( './attribute-matchers.md' ), +} ); + +addStory( { + parents: [ 'reference' ], + name: 'glossary', + title: 'Glossary', + markdown: require( './glossary.md' ), +} ); + +addStory( { + parents: [ 'reference' ], + name: 'design-principles', + title: 'Design Principles', + markdown: require( './design.md' ), +} ); + +addStory( { + parents: [ 'reference' ], + name: 'coding-guidelines', + title: 'Coding Guidelines', + markdown: require( './coding-guidelines.md' ), +} ); + +addStory( { + parents: [ 'reference' ], + name: 'faq', + title: 'Frequently Asked Questions', + markdown: require( './faq.md' ), +} ); diff --git a/docs/readme.md b/docs/readme.md new file mode 100644 index 0000000000000..55afe27762232 --- /dev/null +++ b/docs/readme.md @@ -0,0 +1,13 @@ +# Introduction + +"Gutenberg" is the codename for the 2017 WordPress editor focus. The goal if this focus is to create a new post and page editing experience that makes it easy for anyone to create rich post layouts. This was the kickoff goal: + +> The editor will endeavour to create a new page and post building experience that makes writing rich posts effortless, and has “blocks” to make it easy what today might take shortcodes, custom HTML, or “mystery meat” embed discovery. + +Key take-aways from parsing that paragraph: + +- Authoring richly laid out posts is a key strength of WordPress. +- By embracing "the block", we can potentially unify multiple different interfaces into one. Instead of learning how to write shortcodes, custom HTML, or paste URLs to embed, you should do with just learning the block, and all the pieces should fall in place. +- "Mystery meat" refers to hidden features in software, features that you have to discover. WordPress already supports a large amount of blocks and 30+ embeds, so let's surface them. + +Gutenberg is being developed on [GitHub](https://github.com/WordPress/gutenberg), and you can try [an early beta version today from the plugin repository](https://wordpress.org/plugins/gutenberg/). Though keep in mind it's not fully functional, feature complete, or production ready. diff --git a/docs/reference.md b/docs/reference.md new file mode 100644 index 0000000000000..3313c417fcd69 --- /dev/null +++ b/docs/reference.md @@ -0,0 +1,7 @@ +# Reference + +- [Attribute Matchers](./reference/attribute-matchers) +- [Glossary](./reference/glossary) +- [Design Principles](./reference/design-principles) +- [Coding Guidelines](./reference/coding-guidelines) +- [Frequently Asked Questions](./reference/faq) diff --git a/docutron/.eslintrc.json b/docutron/.eslintrc.json new file mode 100644 index 0000000000000..05ca3cd3a5f8a --- /dev/null +++ b/docutron/.eslintrc.json @@ -0,0 +1,10 @@ +{ + "env": { + "worker": true + }, + "settings": { + "react": { + "pragma": "React" + } + } +} diff --git a/docutron/.gitignore b/docutron/.gitignore new file mode 100644 index 0000000000000..d30f40ef4422f --- /dev/null +++ b/docutron/.gitignore @@ -0,0 +1,21 @@ +# See https://help.github.com/ignore-files/ for more about ignoring files. + +# dependencies +/node_modules + +# testing +/coverage + +# production +/build + +# misc +.DS_Store +.env.local +.env.development.local +.env.test.local +.env.production.local + +npm-debug.log* +yarn-debug.log* +yarn-error.log* diff --git a/docutron/bin/build.js b/docutron/bin/build.js new file mode 100644 index 0000000000000..617372abb1d65 --- /dev/null +++ b/docutron/bin/build.js @@ -0,0 +1,25 @@ +#!/usr/bin/env node + +/** + * External Dependencies + */ +const path = require( 'path' ); + +/** + * Internal Dependencies + */ +const extendsConfig = require( './helpers/extend-webpack-config' ); + +const usersCwd = process.cwd(); +const docsFolder = process.argv[ 2 ]; + +// webpack.config.prod.js checks this. +process.env.NODE_ENV = 'production'; + +// Load and edit the create-react-app config. +process.chdir( path.resolve( __dirname, '../' ) ); +const webpackConfig = require( 'react-scripts/config/webpack.config.prod' ); +extendsConfig( webpackConfig, usersCwd, docsFolder ); + +// Run the build. +require( 'react-scripts/scripts/build' ); diff --git a/docutron/bin/cli.js b/docutron/bin/cli.js new file mode 100755 index 0000000000000..73b4dfc5e610f --- /dev/null +++ b/docutron/bin/cli.js @@ -0,0 +1,20 @@ +#!/usr/bin/env node + +/** + * External Dependencies + */ +const path = require( 'path' ); +const childProcess = require( 'child_process' ); + +const command = process.argv[ 2 ]; +const docsFolder = process.argv[ 3 ] || 'docs'; + +if ( command === 'start' ) { + childProcess.execSync( 'node ' + path.resolve( __dirname, 'start.js ' + docsFolder ), { stdio: [ 0, 1, 2 ] } ); +} else if ( command === 'build' ) { + childProcess.execSync( 'node ' + path.resolve( __dirname, 'build.js ' + docsFolder ), { stdio: [ 0, 1, 2 ] } ); + childProcess.execSync( 'cd ' + path.resolve( __dirname, '../' ) + ' && ./node_modules/.bin/react-snapshot', { stdio: [ 0, 1, 2 ] } ); +} else { + console.error( 'Unknown command ' + command ); // eslint-disable-line no-console + process.exit( 1 ); +} diff --git a/docutron/bin/helpers/extend-webpack-config.js b/docutron/bin/helpers/extend-webpack-config.js new file mode 100644 index 0000000000000..224ea575cf30e --- /dev/null +++ b/docutron/bin/helpers/extend-webpack-config.js @@ -0,0 +1,36 @@ +/** + * External Dependencies + */ +const path = require( 'path' ); + +module.exports = function( webpackConfig, usersCwd, docsFolder ) { + // Adding "docutron" alias + webpackConfig.resolve.alias.docutron = path.resolve( __dirname, '../../src/config/' ); + + // Loading the config folder + webpackConfig.resolve.alias.config = path.resolve( usersCwd, docsFolder ); + webpackConfig.resolve.modules = webpackConfig.resolve.modules.concat( [ webpackConfig.resolve.alias.config ] ); + + // Using the user's node_modules + const usersNodeModules = path.resolve( usersCwd, 'node_modules' ); + webpackConfig.resolve.modules = webpackConfig.resolve.modules.concat( [ usersNodeModules ] ); + + // Deleting CRA scoping + webpackConfig.resolve.plugins = []; + webpackConfig.module.rules.forEach( ( rule ) => { + if ( rule.include ) { + rule.include = [ rule.include, webpackConfig.resolve.alias.config ]; + } + } ); + + // Adding the markdown loader and exclude if from the file loader + webpackConfig.module.rules.forEach( rule => { + if ( rule.loader === require.resolve( 'file-loader' ) ) { + rule.exclude.push( /\.md/ ); + } + } ); + webpackConfig.module.rules.push( { + test: /\.md/, + use: require.resolve( 'raw-loader' ), + } ); +}; diff --git a/docutron/bin/start.js b/docutron/bin/start.js new file mode 100644 index 0000000000000..d72d5a27e4534 --- /dev/null +++ b/docutron/bin/start.js @@ -0,0 +1,25 @@ +#!/usr/bin/env node + +/** + * External Dependencies + */ +const path = require( 'path' ); + +/** + * Internal Dependencies + */ +const extendConfig = require( './helpers/extend-webpack-config' ); + +const usersCwd = process.cwd(); +const docsFolder = process.argv[ 2 ]; + +// webpack.config.prod.js checks this. +process.env.NODE_ENV = 'development'; + +// Load and edit the create-react-app config. +process.chdir( path.resolve( __dirname, '../' ) ); +const webpackConfig = require( 'react-scripts/config/webpack.config.dev' ); +extendConfig( webpackConfig, usersCwd, docsFolder ); + +// Run the build. +require( 'react-scripts/scripts/start' ); diff --git a/docutron/package.json b/docutron/package.json new file mode 100644 index 0000000000000..028386ad1ef94 --- /dev/null +++ b/docutron/package.json @@ -0,0 +1,21 @@ +{ + "name": "docutron", + "version": "5000.0.0", + "dependencies": { + "lodash": "^4.17.4", + "markdown-it": "^8.3.1", + "markdown-it-prism": "^1.1.1", + "markdown-it-toc-and-anchor": "^4.1.2", + "prismjs": "^1.6.0", + "react": "^15.6.1", + "react-dom": "^15.6.1", + "react-router-dom": "^4.1.1", + "react-scripts": "1.0.10", + "react-snapshot": "^1.1.0", + "store": "^2.0.12" + }, + "devDependencies": {}, + "bin": { + "docutron": "./bin/cli.js" + } +} diff --git a/docutron/public/index.html b/docutron/public/index.html new file mode 100644 index 0000000000000..4a04d5ece7745 --- /dev/null +++ b/docutron/public/index.html @@ -0,0 +1,43 @@ + + + + + + + + + + + + + + Gutenberg Docs + + + +
+ + + diff --git a/docutron/public/manifest.json b/docutron/public/manifest.json new file mode 100644 index 0000000000000..588bc83707e27 --- /dev/null +++ b/docutron/public/manifest.json @@ -0,0 +1,8 @@ +{ + "short_name": "Gutenberg Docs", + "name": "Gutenberg Docs", + "start_url": "./index.html", + "display": "standalone", + "theme_color": "#000000", + "background_color": "#ffffff" +} diff --git a/docutron/src/components/app.js b/docutron/src/components/app.js new file mode 100644 index 0000000000000..aefc98d54d210 --- /dev/null +++ b/docutron/src/components/app.js @@ -0,0 +1,37 @@ +/** + * External Dependencies + */ +import React from 'react'; +import { BrowserRouter, Route } from 'react-router-dom'; + +/** + * Internal Dependencies + */ +import { getStories } from '../config'; +import Header from './header'; +import Sidebar from './sidebar'; +import Page from './page'; + +function App() { + return ( + +
+
+
+
+ +
+ { getStories().map( ( story, index ) => ( + + } /> + ) ) } +
+
+
+
+
+ ); +} + +export default App; diff --git a/docutron/src/components/header.js b/docutron/src/components/header.js new file mode 100644 index 0000000000000..1cd20c2093059 --- /dev/null +++ b/docutron/src/components/header.js @@ -0,0 +1,43 @@ +/** + * External Dependencies + */ +import React from 'react'; + +const header = ( + +); + +export default () => header; diff --git a/docutron/src/components/markdown-content.js b/docutron/src/components/markdown-content.js new file mode 100644 index 0000000000000..06c2bc9672449 --- /dev/null +++ b/docutron/src/components/markdown-content.js @@ -0,0 +1,31 @@ +/** + * External dependencies + */ +import React from 'react'; + +/** + * Internal dependencies + */ +import markdown from '../markdown'; +import Tabs from './tabs'; + +function MarkdownContent( { content } ) { + const blocks = markdown( content ); + + return ( +
+ { blocks.map( ( block, index ) => { + if ( block.type === 'raw' ) { + return
; + } + if ( block.type === 'codetabs' ) { + return ; + } + + return null; + } ) } +
+ ); +} + +export default MarkdownContent; diff --git a/docutron/src/components/menu-item.js b/docutron/src/components/menu-item.js new file mode 100644 index 0000000000000..2eec6d413ef7e --- /dev/null +++ b/docutron/src/components/menu-item.js @@ -0,0 +1,44 @@ +/** + * External dependencies + */ +import React from 'react'; +import { Link } from 'react-router-dom'; +import { find } from 'lodash'; + +/** + * Internal dependencies + */ +import { getChildren } from '../config'; + +function MenuItem( { item, searchResults } ) { + const children = getChildren( item.id ); + const isVisible = ! searchResults || + !! find( searchResults, ( story ) => { + return story.id === item.id || + ( story.parents && story.parents.indexOf( item.id ) !== -1 ); + } ); + + if ( ! isVisible ) { + return null; + } + + return ( +
  • + { ! children.length && { item.title } } + { !! children.length && ( +
    + { item.title } +
    + ) } + { !! children.length && ( +
      + { children.map( ( story, index ) => ( + + ) ) } +
    + ) } +
  • + ); +} + +export default MenuItem; diff --git a/docutron/src/components/page.js b/docutron/src/components/page.js new file mode 100644 index 0000000000000..7d53ee93c729f --- /dev/null +++ b/docutron/src/components/page.js @@ -0,0 +1,49 @@ +/** + * External Dependencies + */ +import React, { Component } from 'react'; +import { Link } from 'react-router-dom'; + +/** + * Internal Dependencies + */ +import MarkdownContent from './markdown-content'; +import { getNextStory, getPreviousStory } from '../config'; + +class Page extends Component { + componentDidMount() { + window.scrollTo( 0, 0 ); + } + + render() { + const { story } = this.props; + const nextStory = getNextStory( story.id ); + const previousStory = getPreviousStory( story.id ); + + return ( +
    + { !! story.Component && } + { !! story.markdown && } + +
    + { !! previousStory && ( +

    + + ← { previousStory.title } + +

    + ) } + { !! nextStory && ( +

    + + { nextStory.title } → + +

    + ) } +
    +
    + ); + } +} + +export default Page; diff --git a/docutron/src/components/sidebar.js b/docutron/src/components/sidebar.js new file mode 100644 index 0000000000000..396dd9040379a --- /dev/null +++ b/docutron/src/components/sidebar.js @@ -0,0 +1,73 @@ +/** + * External Dependencies + */ +import React, { Component } from 'react'; + +/** + * Internal Dependencies + */ +import MenuItem from './menu-item'; +import { getStories } from '../config'; + +class Sidebar extends Component { + constructor() { + super( ...arguments ); + this.state = { + searchValue: '', + }; + this.onChangeSearchValue = this.onChangeSearchValue.bind( this ); + } + + onChangeSearchValue( event ) { + this.setState( { searchValue: event.target.value } ); + } + + render() { + const searchResults = this.state.searchValue + ? getStories() + .filter( ( story ) => story.title.toLowerCase().indexOf( this.state.searchValue.toLowerCase() ) !== -1 ) + : null; + + return ( +
    +
    + + + +
    +
    + ); + } +} + +export default Sidebar; diff --git a/docutron/src/components/tabs.js b/docutron/src/components/tabs.js new file mode 100644 index 0000000000000..c92811d561ec0 --- /dev/null +++ b/docutron/src/components/tabs.js @@ -0,0 +1,71 @@ +/** + * External Dependencies + */ +import React, { PureComponent } from 'react'; +import store from 'store'; +import { EventEmitter } from 'events'; + +// Create a shared event emitter so that changes to a preference from one code +// snippet are reflected immediately in all other mounted Tabs components. +const bus = new EventEmitter(); + +class Tabs extends PureComponent { + constructor( props ) { + super( ...arguments ); + + this.onPreferenceChange = this.onPreferenceChange.bind( this ); + + const key = this.getPreferenceKey( props.tabs ); + this.state = { + activeTab: Number( store.get( key ) ) || 0, + }; + } + + componentDidMount() { + bus.on( 'change', this.onPreferenceChange ); + } + + componentWillUnmount() { + bus.removeListener( 'change', this.onPreferenceChange ); + } + + getPreferenceKey( tabs ) { + return 'tabs-preference-' + tabs.map( ( tab ) => tab.name ).join(); + } + + onPreferenceChange( key, index ) { + if ( key === this.getPreferenceKey( this.props.tabs ) ) { + this.setState( { activeTab: index } ); + } + } + + selectTab( index ) { + return () => { + const key = this.getPreferenceKey( this.props.tabs ); + store.set( key, index ); + bus.emit( 'change', key, index ); + }; + } + + render() { + const { tabs } = this.props; + const activeTab = tabs[ this.state.activeTab ]; + + return ( +
    + { tabs.map( ( tab, index ) => ( + + ) ) } + { activeTab &&
    } +
    + ); + } +} + +export default Tabs; diff --git a/docutron/src/config/index.js b/docutron/src/config/index.js new file mode 100644 index 0000000000000..49f3975f9774f --- /dev/null +++ b/docutron/src/config/index.js @@ -0,0 +1,49 @@ +/** + * External Dependencies + */ +import { find } from 'lodash'; + +const stories = []; + +export function addStory( story ) { + const { name, parents = [] } = story; + stories.push( { + path: '/' + parents.concat( name ).join( '/' ) + '/', + id: parents.concat( name ).join( '.' ), + parent: parents.join( '.' ), + ...story, + } ); +} + +export function getStories() { + return stories; +} + +export function getStory( id ) { + return find( stories, ( story ) => story.id === id ); +} + +export function getChildren( id ) { + return stories.filter( ( story ) => story.parent === id ); +} + +export function getOrderedPageList( parentId = '' ) { + return getChildren( parentId ).reduce( ( memo, story ) => { + memo.push( story ); + return memo.concat( getOrderedPageList( story.id ) ); + }, [] ); +} + +export function getNextStory( id ) { + const orderedList = getOrderedPageList(); + const index = orderedList.indexOf( getStory( id ) ); + + return orderedList[ index + 1 ]; +} + +export function getPreviousStory( id ) { + const orderedList = getOrderedPageList(); + const index = orderedList.indexOf( getStory( id ) ); + + return orderedList[ index - 1 ]; +} diff --git a/docutron/src/index.js b/docutron/src/index.js new file mode 100644 index 0000000000000..6051f571d6a71 --- /dev/null +++ b/docutron/src/index.js @@ -0,0 +1,19 @@ +/** + * External Dependencies + */ +import React from 'react'; +import { render } from 'react-snapshot'; +import 'prismjs/themes/prism.css'; + +/** + * User Dependencies + */ +import 'config'; + +/** + * Internal Dependencies + */ +import App from './components/app'; +import './styles/main.css'; + +render( , document.getElementById( 'root' ) ); diff --git a/docutron/src/markdown/index.js b/docutron/src/markdown/index.js new file mode 100644 index 0000000000000..0aa15b08c1fc6 --- /dev/null +++ b/docutron/src/markdown/index.js @@ -0,0 +1,61 @@ +/** + * External Dependencies + */ +import MarkdownIt from 'markdown-it'; +import markdownItPrismPlugin from 'markdown-it-prism'; +import markdownItTOCAndAnchorPlugin from 'markdown-it-toc-and-anchor'; +import { compact } from 'lodash'; + +const parser = new MarkdownIt( { + html: true, +} ); +parser + .use( markdownItTOCAndAnchorPlugin ) + .use( markdownItPrismPlugin ); + +const blockParsers = { + raw( content ) { + return { + type: 'raw', + content: parser.render( content ), + }; + }, + + codetabs( content ) { + const tabsRegex = /{%\s+([\w]+)\s+%}/gm; + const splittedTabs = compact( content.trim().split( tabsRegex ) ); + const tabs = []; + for ( let i = 0; i < splittedTabs.length; i = i + 2 ) { + tabs.push( { + name: splittedTabs[ i ], + content: parser.render( splittedTabs[ i + 1 ] ), + } ); + } + + return { + type: 'codetabs', + tabs, + }; + }, +}; + +function parse( markdown ) { + const blocksRegex = /({%\s+[\w]+\s+%}(?:.|\n|\r)*?{%\s+end\s+%})/gm; + const blockRegex = /{%\s+([\w]+)\s+%}((?:.|\n|\r)*?){%\s+end\s+%}/gm; + const blocks = markdown.split( blocksRegex ); + return blocks + .map( ( block ) => { + const matches = blockRegex.exec( block ); + if ( ! matches ) { + return { type: 'raw', content: block }; + } + return { type: matches[ 1 ], content: matches[ 2 ] }; + } ) + .map( ( block ) => { + const blockParser = blockParsers[ block.type ] || blockParsers.raw; + + return blockParser( block.content ); + } ); +} + +export default parse; diff --git a/docutron/src/styles/main.css b/docutron/src/styles/main.css new file mode 100644 index 0000000000000..1cf3e4eca3ab7 --- /dev/null +++ b/docutron/src/styles/main.css @@ -0,0 +1,29 @@ +body { + margin: 0; +} + +#secondary { + padding: 4rem 0 0; +} + +code { + tab-size: 4; +} + +:not(pre) > code[class*="language-"], +pre[class*="language-"] { + background-color: #f7f7f7 !important; +} + +.components-code-tab { + border-radius: 0; +} + +.components-code-tab.is-active { + border-color: #cccccc; + box-shadow: inset 0 -1px 0 rgba(255, 255, 255, 0.5), inset 0 2px 5px rgba(0, 0, 0, 0.15); +} + +.widget_search { + background: transparent; +} diff --git a/editor/story/index.js b/editor/story/index.js deleted file mode 100644 index 08cbfea30a119..0000000000000 --- a/editor/story/index.js +++ /dev/null @@ -1,15 +0,0 @@ -/** - * External dependencies - */ -import ReactMarkdown from 'react-markdown'; -import { storiesOf } from '@storybook/react'; -import { withKnobs } from '@storybook/addon-knobs'; - -/** - * Internal dependencies - */ -import readme from '../README.md'; - -storiesOf( 'Modules', module ) - .addDecorator( withKnobs ) - .add( 'Editor', () => ); diff --git a/element/story/index.js b/element/story/index.js deleted file mode 100644 index 31ec383f04352..0000000000000 --- a/element/story/index.js +++ /dev/null @@ -1,15 +0,0 @@ -/** - * External dependencies - */ -import ReactMarkdown from 'react-markdown'; -import { storiesOf } from '@storybook/react'; -import { withKnobs } from '@storybook/addon-knobs'; - -/** - * Internal dependencies - */ -import readme from '../README.md'; - -storiesOf( 'Modules', module ) - .addDecorator( withKnobs ) - .add( 'Element', () => ); diff --git a/i18n/story/index.js b/i18n/story/index.js deleted file mode 100644 index 459d414852175..0000000000000 --- a/i18n/story/index.js +++ /dev/null @@ -1,15 +0,0 @@ -/** - * External dependencies - */ -import ReactMarkdown from 'react-markdown'; -import { storiesOf } from '@storybook/react'; -import { withKnobs } from '@storybook/addon-knobs'; - -/** - * Internal dependencies - */ -import readme from '../README.md'; - -storiesOf( 'Modules', module ) - .addDecorator( withKnobs ) - .add( 'i18n', () => ); diff --git a/package.json b/package.json index bd6201cf641c7..efbac118e9c53 100644 --- a/package.json +++ b/package.json @@ -37,10 +37,6 @@ "uuid": "^3.0.1" }, "devDependencies": { - "@storybook/addon-info": "^3.0.1", - "@storybook/addon-knobs": "^3.0.1", - "@storybook/addon-options": "^3.0.1", - "@storybook/react": "^3.0.0", "autoprefixer": "^6.7.7", "babel-core": "^6.24.0", "babel-eslint": "^7.2.0", @@ -130,13 +126,13 @@ "scripts": { "build": "cross-env BABEL_ENV=default NODE_ENV=production webpack", "gettext-strings": "cross-env BABEL_ENV=gettext webpack", - "lint": "eslint . .storybook", + "lint": "eslint .", "dev": "cross-env BABEL_ENV=default webpack --watch", "test": "npm run lint && npm run test-unit", "ci": "concurrently \"npm run lint && npm run build\" \"npm run test-unit:coverage-ci\"", "package-plugin": "./bin/build-plugin-zip.sh", - "storybook": "start-storybook -p 6006", - "build-storybook": "build-storybook", + "docs-start": "./docutron/bin/cli.js start", + "docs-build": "./docutron/bin/cli.js build", "test-unit": "jest", "test-unit:coverage": "jest --coverage", "test-unit:coverage-ci": "jest --coverage --maxWorkers 1 && coveralls < coverage/lcov.info",