From 6302319d7088f1f1810b3dd9183784e2f956425f Mon Sep 17 00:00:00 2001 From: Gary Pendergast Date: Wed, 4 Oct 2017 21:30:46 +1100 Subject: [PATCH 1/5] First pass at creating a dynamically generated Table of Contents block --- blocks/library/heading/index.js | 2 +- blocks/library/index.js | 1 + blocks/library/table-of-contents/editor.scss | 2 + blocks/library/table-of-contents/index.js | 75 ++++++++++++++ blocks/library/table-of-contents/index.php | 102 +++++++++++++++++++ blocks/library/table-of-contents/style.scss | 28 +++++ 6 files changed, 209 insertions(+), 1 deletion(-) create mode 100644 blocks/library/table-of-contents/editor.scss create mode 100644 blocks/library/table-of-contents/index.js create mode 100644 blocks/library/table-of-contents/index.php create mode 100644 blocks/library/table-of-contents/style.scss diff --git a/blocks/library/heading/index.js b/blocks/library/heading/index.js index 5e4f01dc44d5a..4bee10ab5a0c9 100644 --- a/blocks/library/heading/index.js +++ b/blocks/library/heading/index.js @@ -167,7 +167,7 @@ registerBlockType( 'core/heading', { const Tag = nodeName.toLowerCase(); return ( - + { content } ); diff --git a/blocks/library/index.js b/blocks/library/index.js index 8ae8ce98543f2..ab21c78d78e35 100644 --- a/blocks/library/index.js +++ b/blocks/library/index.js @@ -22,3 +22,4 @@ import './text-columns'; import './verse'; import './video'; import './audio'; +import './table-of-contents'; diff --git a/blocks/library/table-of-contents/editor.scss b/blocks/library/table-of-contents/editor.scss new file mode 100644 index 0000000000000..41481d7d90755 --- /dev/null +++ b/blocks/library/table-of-contents/editor.scss @@ -0,0 +1,2 @@ +div[data-type="core/table-of-contents"] { +} diff --git a/blocks/library/table-of-contents/index.js b/blocks/library/table-of-contents/index.js new file mode 100644 index 0000000000000..8a9298d1bbed2 --- /dev/null +++ b/blocks/library/table-of-contents/index.js @@ -0,0 +1,75 @@ +/** + * WordPress + */ +import { __ } from '@wordpress/i18n'; + +/** + * Internal dependencies + */ +import './editor.scss'; +import './style.scss'; +import { registerBlockType } from '../../api'; +import InspectorControls from '../../inspector-controls'; +import TextControl from '../../inspector-controls/text-control'; +import ToggleControl from '../../inspector-controls/toggle-control'; +import BlockDescription from '../../block-description'; + +registerBlockType( 'core/table-of-contents', { + title: __( 'Table of Contents' ), + + icon: 'list-view', + + category: 'widgets', + + attributes: { + title: { + type: 'string', + default: __( 'Table of Contents' ), + }, + numbered: { + type: 'bool', + default: true, + }, + }, + + edit( { attributes, setAttributes, focus, setFocus, className } ) { + const { title, numbered } = attributes; + return [ + focus && ( + + +

{ __( 'This is a table of contents, y\'all' ) }

+
+ +

{ __( 'Table of Contents Settings' ) }

+ + setAttributes( { + title: value, + } ) + } + /> + + setAttributes( { + numbered: ! numbered, + } ) + } + /> +
+ ), + __( 'Here shall render ye table of contents' ), + ]; + }, + + save( { attributes } ) { + return null; + }, +} ); diff --git a/blocks/library/table-of-contents/index.php b/blocks/library/table-of-contents/index.php new file mode 100644 index 0000000000000..504bc6f367ec1 --- /dev/null +++ b/blocks/library/table-of-contents/index.php @@ -0,0 +1,102 @@ +post_content ) ) { + return ''; + } + + $blocks = gutenberg_parse_blocks( $post->post_content ); + $headings = array_filter( $blocks, 'filter_block_array_for_headings' ); + + if ( ! $headings ) { + return ''; + } + + $html = ''; + + $levelCounts = array( + 1 => 0, + 2 => 0, + 3 => 0, + 4 => 0, + 5 => 0, + 6 => 0, + ); + + foreach ( $headings as $heading ) { + $content = trim( $heading['rawContent'] ); + if ( ! $content ) { + continue; + } + + preg_match( '/^'; + + } + + if ( $html ) { + $html = "
    $html
"; + return "

{$attributes['title']}

$html"; + } + + return ''; +} + +function filter_block_array_for_headings( $block ) { + return 'core/heading' === $block['blockName']; +} + +function create_level_string( $levelCounts, $level ) { + $string = ''; + for ( $ii = 2; $ii <= $level; $ii++ ) { + $string .= $levelCounts[ $ii ]; + if ( $ii != $level ) { + $string .= '.'; + } + } + + for ( ; $ii <= 6; $ii++ ) { + $levelCounts[ $ii ] = 0; + } + + return $string; +} + +register_block_type( 'core/table-of-contents', array( + 'attributes' => array( + 'title' => array( + 'type' => 'string', + 'default' => __( 'Table of Contents', 'gutenberg' ), + ), + 'numbered' => array( + 'type' => 'bool', + 'default' => true, + ), + ), + 'render_callback' => 'gutenberg_render_block_core_table_of_contents', +) ); diff --git a/blocks/library/table-of-contents/style.scss b/blocks/library/table-of-contents/style.scss new file mode 100644 index 0000000000000..92e0b30d34f00 --- /dev/null +++ b/blocks/library/table-of-contents/style.scss @@ -0,0 +1,28 @@ +.wp-block-table-of-contents { + list-style: none; + + .level1 { + margin-left: 10px; + } + + .level2 { + margin-left: 20px; + } + + .level3 { + margin-left: 30px; + } + + .level4 { + margin-left: 40px; + } + + .level5 { + margin-left: 50px; + } + + .level6 { + margin-left: 60px; + } + +} From 498a591fb83aee929eb3ed0ed00902b5fa803e04 Mon Sep 17 00:00:00 2001 From: Gary Pendergast Date: Wed, 4 Oct 2017 21:51:28 +1100 Subject: [PATCH 2/5] Fix the linting errors --- blocks/library/heading/index.js | 2 +- blocks/library/table-of-contents/index.js | 4 +-- blocks/library/table-of-contents/index.php | 33 ++++++++++++++++------ 3 files changed, 27 insertions(+), 12 deletions(-) diff --git a/blocks/library/heading/index.js b/blocks/library/heading/index.js index 4bee10ab5a0c9..1a831aae39d60 100644 --- a/blocks/library/heading/index.js +++ b/blocks/library/heading/index.js @@ -167,7 +167,7 @@ registerBlockType( 'core/heading', { const Tag = nodeName.toLowerCase(); return ( - + { content } ); diff --git a/blocks/library/table-of-contents/index.js b/blocks/library/table-of-contents/index.js index 8a9298d1bbed2..387edf8512b52 100644 --- a/blocks/library/table-of-contents/index.js +++ b/blocks/library/table-of-contents/index.js @@ -32,7 +32,7 @@ registerBlockType( 'core/table-of-contents', { }, }, - edit( { attributes, setAttributes, focus, setFocus, className } ) { + edit( { attributes, setAttributes, focus } ) { const { title, numbered } = attributes; return [ focus && ( @@ -69,7 +69,7 @@ registerBlockType( 'core/table-of-contents', { ]; }, - save( { attributes } ) { + save() { return null; }, } ); diff --git a/blocks/library/table-of-contents/index.php b/blocks/library/table-of-contents/index.php index 504bc6f367ec1..a954561d0bfae 100644 --- a/blocks/library/table-of-contents/index.php +++ b/blocks/library/table-of-contents/index.php @@ -21,14 +21,14 @@ function gutenberg_render_block_core_table_of_contents( $attributes ) { $blocks = gutenberg_parse_blocks( $post->post_content ); $headings = array_filter( $blocks, 'filter_block_array_for_headings' ); - + if ( ! $headings ) { return ''; } $html = ''; - $levelCounts = array( + $level_counts = array( 1 => 0, 2 => 0, 3 => 0, @@ -48,14 +48,14 @@ function gutenberg_render_block_core_table_of_contents( $attributes ) { continue; } - $levelCounts[ $matches[1] ]++; + $level_counts[ $matches[1] ]++; - $levelString = ''; + $level_string = ''; if ( $attributes['numbered'] ) { - $levelString = create_level_string( $levelCounts, $matches[1] ) . ' '; + $level_string = create_level_string( $level_counts, $matches[1] ) . ' '; } - $html .= "
  • $levelString" . wp_strip_all_tags( $content ) . '
  • '; + $html .= "
  • $level_string" . wp_strip_all_tags( $content ) . '
  • '; } @@ -67,21 +67,36 @@ function gutenberg_render_block_core_table_of_contents( $attributes ) { return ''; } +/** + * Filter function for array_filter(), to remove all blocks except core/heading blocks. + * + * @param array $block The block to check. + * + * @return bool Whether the block is a core/heading block or not. + */ function filter_block_array_for_headings( $block ) { return 'core/heading' === $block['blockName']; } -function create_level_string( $levelCounts, $level ) { +/** + * Creates a TOC chapter string, based on where the parser is currently up to. + * + * @param array $level_counts The state of each chapter level. + * @param int $level The currently chapter's level. + * + * @return string The chapter string. + */ +function create_level_string( $level_counts, $level ) { $string = ''; for ( $ii = 2; $ii <= $level; $ii++ ) { - $string .= $levelCounts[ $ii ]; + $string .= $level_counts[ $ii ]; if ( $ii != $level ) { $string .= '.'; } } for ( ; $ii <= 6; $ii++ ) { - $levelCounts[ $ii ] = 0; + $level_counts[ $ii ] = 0; } return $string; From d6733d694d85b955bf8bd2a19bcd4eb48ab639c9 Mon Sep 17 00:00:00 2001 From: Gary Pendergast Date: Mon, 9 Oct 2017 16:11:58 +1100 Subject: [PATCH 3/5] Masses of code cleanup. --- blocks/library/heading/index.js | 2 +- .../class-block-table-of-contents.php | 199 ++++++++++++++++++ blocks/library/table-of-contents/index.php | 115 +--------- lib/blocks.php | 9 +- 4 files changed, 212 insertions(+), 113 deletions(-) create mode 100644 blocks/library/table-of-contents/class-block-table-of-contents.php diff --git a/blocks/library/heading/index.js b/blocks/library/heading/index.js index 1a831aae39d60..5e4f01dc44d5a 100644 --- a/blocks/library/heading/index.js +++ b/blocks/library/heading/index.js @@ -167,7 +167,7 @@ registerBlockType( 'core/heading', { const Tag = nodeName.toLowerCase(); return ( - + { content } ); diff --git a/blocks/library/table-of-contents/class-block-table-of-contents.php b/blocks/library/table-of-contents/class-block-table-of-contents.php new file mode 100644 index 0000000000000..b875c8de156f8 --- /dev/null +++ b/blocks/library/table-of-contents/class-block-table-of-contents.php @@ -0,0 +1,199 @@ + array( + 'title' => array( + 'type' => 'string', + 'default' => __( 'Table of Contents', 'gutenberg' ), + ), + 'numbered' => array( + 'type' => 'bool', + 'default' => true, + ), + ), + 'render_callback' => array( $this, 'add_placeholder' ), + ) ); + + add_filter( 'raw_block_content', array( $this, 'add_id_to_heading_blocks' ), 10, 2 ); + add_filter( 'the_content', array( $this, 'insert_toc' ) ); + } + + /** + * Initialises an instance of the class. + * + * @return Gutenberg_Table_of_Contents Instance of the class. + */ + static public function init() { + static $instance; + if ( ! $instance ) { + $instance = new Gutenberg_Table_of_Contents(); + } + return $instance; + } + + /** + * Adds a placeholder for the Table of Contents to be rendered into, after + * all of the blocks have been processed. + * + * @param array $attributes The block attributes. + * + * @return string The placeholder. + */ + public function add_placeholder( $attributes ) { + $toc_id = ''; + + $this->ids[] = $toc_id; + $this->titles[] = $attributes['title']; + $this->numbered[] = $attributes['numbered']; + + return $toc_id; + } + + /** + * Replaces the Table of Contents placeholders with the actual Table of Contents + * + * @param string $content The HTML content of the post. + * + * @return string The post HTML, with Table of Contents inserted. + */ + public function insert_toc( $content ) { + foreach ( $this->ids as $count => $id ) { + $title = $this->titles[ $count ]; + $numbered = $this->numbered[ $count ]; + + $html = "

    $title

    "; + + if ( ! $this->headings ) { + $html .= '

    ' . __( 'Empty', 'gutenberg' ) . '

    '; + $content = str_replace( $id, $html, $content ); + continue; + } + + $html .= '
      '; + + $level_counts = array( + 1 => 0, + 2 => 0, + 3 => 0, + 4 => 0, + 5 => 0, + 6 => 0, + ); + + foreach ( $this->headings as $heading ) { + $level_string = ''; + if ( $numbered ) { + $level_counts[ $heading['level'] ]++; + for ( $ii = $heading['level'] + 1; $ii <= 6; $ii++ ) { + $level_counts[ $ii ] = 0; + } + $level_string = $this->create_chapter_string( $level_counts, $heading['level'] ) . ' '; + } + + $html .= "
    • $level_string{$heading['heading']}
    • "; + } + + $content = str_replace( $id, $html, $content ); + } + + return $content; + } + + /** + * Creates a TOC chapter string, based on where the parser is currently up to. + * + * @param array $level_counts The state of each chapter level. + * @param int $level The currently chapter's level. + * + * @return string The chapter string. + */ + private function create_chapter_string( $level_counts, $level ) { + $string = ''; + for ( $ii = 2; $ii <= $level; $ii++ ) { + $string .= $level_counts[ $ii ]; + if ( $ii != $level ) { + $string .= '.'; + } + } + return $string; + } + + /** + * When a heading block is processed, we need to add an ID attribute, so we can link to it. + * + * @param string $content The raw content of the block being processed. + * @param string $block_name The Block Name of the block being processed. + * + * @return string The HTML to replace $content with. + */ + public function add_id_to_heading_blocks( $content, $block_name ) { + if ( 'core/heading' !== $block_name ) { + return $content; + } + + return preg_replace_callback( '|^(\s*)(.+)|i', array( $this, 'add_id_to_heading_blocks_callback' ), $content ); + } + + /** + * Internal callback for add an ID to headers. + * + * @see Gutenberg_Table_of_Contents::add_id_to_heading_blocks() + * + * @param array $matches Array of matches. + * + * @return string The replacement string to use. + */ + private function add_id_to_heading_blocks_callback( $matches ) { + $heading = trim( wp_strip_all_tags( $matches[3], true ) ); + $id = 'heading-' . preg_replace( '/[^a-z0-9_]+/i', '-', $heading ); + + $this->headings[] = array( + 'level' => $matches[2], + 'heading' => $heading, + 'id' => $id, + ); + + return "{$matches[1]}{$matches[3]}"; + } +} diff --git a/blocks/library/table-of-contents/index.php b/blocks/library/table-of-contents/index.php index a954561d0bfae..c02cd5beeed61 100644 --- a/blocks/library/table-of-contents/index.php +++ b/blocks/library/table-of-contents/index.php @@ -1,117 +1,10 @@ post_content ) ) { - return ''; - } - - $blocks = gutenberg_parse_blocks( $post->post_content ); - $headings = array_filter( $blocks, 'filter_block_array_for_headings' ); - - if ( ! $headings ) { - return ''; - } - - $html = ''; - - $level_counts = array( - 1 => 0, - 2 => 0, - 3 => 0, - 4 => 0, - 5 => 0, - 6 => 0, - ); - - foreach ( $headings as $heading ) { - $content = trim( $heading['rawContent'] ); - if ( ! $content ) { - continue; - } - - preg_match( '/^'; - - } - - if ( $html ) { - $html = "
        $html
      "; - return "

      {$attributes['title']}

      $html"; - } - - return ''; -} - -/** - * Filter function for array_filter(), to remove all blocks except core/heading blocks. - * - * @param array $block The block to check. - * - * @return bool Whether the block is a core/heading block or not. - */ -function filter_block_array_for_headings( $block ) { - return 'core/heading' === $block['blockName']; -} - -/** - * Creates a TOC chapter string, based on where the parser is currently up to. - * - * @param array $level_counts The state of each chapter level. - * @param int $level The currently chapter's level. - * - * @return string The chapter string. - */ -function create_level_string( $level_counts, $level ) { - $string = ''; - for ( $ii = 2; $ii <= $level; $ii++ ) { - $string .= $level_counts[ $ii ]; - if ( $ii != $level ) { - $string .= '.'; - } - } - - for ( ; $ii <= 6; $ii++ ) { - $level_counts[ $ii ] = 0; - } - - return $string; -} - -register_block_type( 'core/table-of-contents', array( - 'attributes' => array( - 'title' => array( - 'type' => 'string', - 'default' => __( 'Table of Contents', 'gutenberg' ), - ), - 'numbered' => array( - 'type' => 'bool', - 'default' => true, - ), - ), - 'render_callback' => 'gutenberg_render_block_core_table_of_contents', -) ); +// Load the Table of Contents class. +require_once './class-block-table-of-contents.php'; +add_action( 'init', array( 'Gutenberg_Table_Of_Contents', 'init' ) ); diff --git a/lib/blocks.php b/lib/blocks.php index dc329e94ef6f5..7c562dc4680a5 100644 --- a/lib/blocks.php +++ b/lib/blocks.php @@ -86,7 +86,14 @@ function do_blocks( $content ) { } if ( $raw_content ) { - $content_after_blocks .= $raw_content; + /** + * Filters the raw HTML produced by an individual block. + * + * @param string $raw_content The raw HTML produced by the block. + * @param string $block_name The block name. + * @param array $attributes The block's attributes. + */ + $content_after_blocks .= apply_filters( 'raw_block_content', $raw_content, $block_name, $attributes ); } } From 69e87d45d896147a9ddf927b1187b0da8de6dbce Mon Sep 17 00:00:00 2001 From: Gary Pendergast Date: Mon, 9 Oct 2017 16:40:48 +1100 Subject: [PATCH 4/5] Fix all the test failures I caused. --- .../class-block-table-of-contents.php | 4 ++-- blocks/library/table-of-contents/index.php | 4 ++-- blocks/test/fixtures/core__table-of-contents.html | 1 + blocks/test/fixtures/core__table-of-contents.json | 12 ++++++++++++ .../fixtures/core__table-of-contents.parsed.json | 9 +++++++++ .../fixtures/core__table-of-contents.serialized.html | 1 + lib/blocks.php | 2 +- 7 files changed, 28 insertions(+), 5 deletions(-) create mode 100644 blocks/test/fixtures/core__table-of-contents.html create mode 100644 blocks/test/fixtures/core__table-of-contents.json create mode 100644 blocks/test/fixtures/core__table-of-contents.parsed.json create mode 100644 blocks/test/fixtures/core__table-of-contents.serialized.html diff --git a/blocks/library/table-of-contents/class-block-table-of-contents.php b/blocks/library/table-of-contents/class-block-table-of-contents.php index b875c8de156f8..aa892c58772c5 100644 --- a/blocks/library/table-of-contents/class-block-table-of-contents.php +++ b/blocks/library/table-of-contents/class-block-table-of-contents.php @@ -62,12 +62,12 @@ function __construct() { /** * Initialises an instance of the class. * - * @return Gutenberg_Table_of_Contents Instance of the class. + * @return Block_Table_Of_Contents Instance of the class. */ static public function init() { static $instance; if ( ! $instance ) { - $instance = new Gutenberg_Table_of_Contents(); + $instance = new Block_Table_Of_Contents(); } return $instance; } diff --git a/blocks/library/table-of-contents/index.php b/blocks/library/table-of-contents/index.php index c02cd5beeed61..15ba602ff29ff 100644 --- a/blocks/library/table-of-contents/index.php +++ b/blocks/library/table-of-contents/index.php @@ -6,5 +6,5 @@ */ // Load the Table of Contents class. -require_once './class-block-table-of-contents.php'; -add_action( 'init', array( 'Gutenberg_Table_Of_Contents', 'init' ) ); +require_once dirname( __FILE__ ) . '/class-block-table-of-contents.php'; +add_action( 'init', array( 'Block_Table_Of_Contents', 'init' ) ); diff --git a/blocks/test/fixtures/core__table-of-contents.html b/blocks/test/fixtures/core__table-of-contents.html new file mode 100644 index 0000000000000..280fc8b658c5e --- /dev/null +++ b/blocks/test/fixtures/core__table-of-contents.html @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/blocks/test/fixtures/core__table-of-contents.json b/blocks/test/fixtures/core__table-of-contents.json new file mode 100644 index 0000000000000..af29eadd93347 --- /dev/null +++ b/blocks/test/fixtures/core__table-of-contents.json @@ -0,0 +1,12 @@ +[ + { + "uid": "_uid_0", + "name": "core/table-of-contents", + "isValid": true, + "attributes": { + "title": "Table of Contents", + "numbered": true + }, + "originalContent": "" + } +] diff --git a/blocks/test/fixtures/core__table-of-contents.parsed.json b/blocks/test/fixtures/core__table-of-contents.parsed.json new file mode 100644 index 0000000000000..8d93c658a11de --- /dev/null +++ b/blocks/test/fixtures/core__table-of-contents.parsed.json @@ -0,0 +1,9 @@ +[ + { + "blockName": "core/table-of-contents", + "attrs": { + "title": "Table of Contents" + }, + "rawContent": "" + } +] diff --git a/blocks/test/fixtures/core__table-of-contents.serialized.html b/blocks/test/fixtures/core__table-of-contents.serialized.html new file mode 100644 index 0000000000000..c07afd290aa83 --- /dev/null +++ b/blocks/test/fixtures/core__table-of-contents.serialized.html @@ -0,0 +1 @@ + diff --git a/lib/blocks.php b/lib/blocks.php index 7c562dc4680a5..91ee2395a3b57 100644 --- a/lib/blocks.php +++ b/lib/blocks.php @@ -88,7 +88,7 @@ function do_blocks( $content ) { if ( $raw_content ) { /** * Filters the raw HTML produced by an individual block. - * + * * @param string $raw_content The raw HTML produced by the block. * @param string $block_name The block name. * @param array $attributes The block's attributes. From 1dae5ea91e4ccb93c3699aac02a11d57a807e8c4 Mon Sep 17 00:00:00 2001 From: Gary Pendergast Date: Mon, 9 Oct 2017 17:49:59 +1100 Subject: [PATCH 5/5] Make the TOC heading editable --- blocks/editable/index.js | 6 +++--- blocks/library/table-of-contents/index.js | 25 +++++++++++------------ 2 files changed, 15 insertions(+), 16 deletions(-) diff --git a/blocks/editable/index.js b/blocks/editable/index.js index 7198db9f50d83..67ac656cb0b55 100644 --- a/blocks/editable/index.js +++ b/blocks/editable/index.js @@ -613,7 +613,7 @@ export default class Editable extends Component { const isPlaceholderVisible = placeholder && ( ! focus || keepPlaceholderOnFocus ) && this.state.empty; const classes = classnames( wrapperClassname, 'blocks-editable' ); - const formatToolbar = ( + const formatToolbar = formattingControls && ( - { focus && + { focus && formatToolbar && { ! inlineToolbar && formatToolbar } } - { focus && inlineToolbar && + { focus && inlineToolbar && formatToolbar &&
      { formatToolbar }
      diff --git a/blocks/library/table-of-contents/index.js b/blocks/library/table-of-contents/index.js index 387edf8512b52..9bd3963d2c32d 100644 --- a/blocks/library/table-of-contents/index.js +++ b/blocks/library/table-of-contents/index.js @@ -9,8 +9,8 @@ import { __ } from '@wordpress/i18n'; import './editor.scss'; import './style.scss'; import { registerBlockType } from '../../api'; +import Editable from '../../editable'; import InspectorControls from '../../inspector-controls'; -import TextControl from '../../inspector-controls/text-control'; import ToggleControl from '../../inspector-controls/toggle-control'; import BlockDescription from '../../block-description'; @@ -32,7 +32,7 @@ registerBlockType( 'core/table-of-contents', { }, }, - edit( { attributes, setAttributes, focus } ) { + edit( { attributes, setAttributes, focus, setFocus } ) { const { title, numbered } = attributes; return [ focus && ( @@ -43,17 +43,6 @@ registerBlockType( 'core/table-of-contents', {

      { __( 'Table of Contents Settings' ) }

      - setAttributes( { - title: value, - } ) - } - /> - ), + setAttributes( { title: value[ 0 ] } ) } + formattingControls={ false } + multiline={ false } + />, __( 'Here shall render ye table of contents' ), ]; },