diff --git a/core/core.services.yml b/core/core.services.yml index 6049d3b..e2705fc 100644 --- a/core/core.services.yml +++ b/core/core.services.yml @@ -587,6 +587,9 @@ services: plugin.manager.action: class: Drupal\Core\Action\ActionManager arguments: ['@container.namespaces', '@cache.discovery', '@module_handler'] + plugin.manager.text_section: + class: Drupal\Core\Render\TextSectionManager + parent: default_plugin_manager plugin.manager.menu.link: class: Drupal\Core\Menu\MenuLinkManager arguments: ['@menu.tree_storage', '@menu_link.static.overrides', '@module_handler'] diff --git a/core/lib/Drupal/Core/Render/Annotation/TextSection.php b/core/lib/Drupal/Core/Render/Annotation/TextSection.php new file mode 100644 index 0000000..746f275 --- /dev/null +++ b/core/lib/Drupal/Core/Render/Annotation/TextSection.php @@ -0,0 +1,57 @@ + 'text_sections', + * '#sections' => [ + * [ + * '#section_type' => 'heading', + * '#text' => ['#markup' => t('Greetings')], + * ], + * [ + * '#section_type' => 'paragraph', + * '#text' => ['#markup' => t('Hello, world.')], + * ], + * ], + * ]; + * @endcode + * + * @see \Drupal\Core\Render\TextSectionPluginInterface + * + * @RenderElement("text_sections") + */ +class TextSections extends RenderElement { + + /** + * {@inheritdoc} + */ + public function getInfo() { + $class = get_class($this); + return [ + '#pre_render' => [[$class, 'preRenderSections']], + ]; + } + + /** + * Expands a #text_sections element into an array of sections. + * + * @param array $element + * The form element to process. See main class documentation for properties. + * + * @return array + * The form element. + */ + public static function preRenderSections(&$element) { + // Iterate through the provided sections, adding each as a new render + // array child to $element, or as part of a grouped render array. + $sections = $element['#sections']; + unset($element['#sections']); + unset($element['#type']); + $manager = self::sectionManager(); + $element['text_sections'] = []; + + $previous_type = ''; + $group_build = []; + + /** @var \Drupal\Core\Render\RendererInterface $renderer */ + $renderer = \Drupal::service('renderer'); + foreach ($sections as $section) { + /** @var \Drupal\Core\Render\TextSectionPluginInterface $plugin */ + $plugin = $manager->createInstance($section['#section_type']); + if (!$plugin) { + // Do not attempt to render unknown types. Just skip them. + continue; + } + + // Set up the inner build. + $renderer->addCacheableDependency($element, $plugin); + $plugin->setText($section['#text']); + + // Check for grouping. + $this_group_with = $plugin->groupWith(); + + if (!in_array($previous_type, $this_group_with) && !empty($group_build)) { + // This section should not be grouped with the previous one, so add the + // previous grouping to the element and reset the in-progress group. + $element['text_sections'][] = $group_build; + $group_build = []; + } + + if (!empty($this_group_with)) { + // This is a grouped section, either for a new group or a compatible + // group. Add this to the group, or initialize a new group. + $group_build = $plugin->addToGroup($group_build); + } + else { + // It is not grouped, so just add it to the element. + $element['text_sections'][] = $plugin->getInnerBuild(); + } + + // Save this type for the next grouping decision. + $previous_type = $section['#section_type']; + } + + // Add the last in-progress group to the element if there is one. + if (!empty($group_build)) { + $element['text_sections'][] = $group_build; + } + + return $element; + } + + /** + * Wraps the text section manager service. + * + * @return \Drupal\Core\Render\TextSectionManager + * The text section plugin manager. + */ + protected static function sectionManager() { + return \Drupal::service('plugin.manager.text_section'); + } + +} diff --git a/core/lib/Drupal/Core/Render/Plugin/TextSection/BulletItem.php b/core/lib/Drupal/Core/Render/Plugin/TextSection/BulletItem.php new file mode 100644 index 0000000..3dd9aaf --- /dev/null +++ b/core/lib/Drupal/Core/Render/Plugin/TextSection/BulletItem.php @@ -0,0 +1,39 @@ + 'item_list', + '#list_type' => 'ul', + '#items' => [], + ]; + + $group['#items'][] = $this->getInnerBuild(); + + return $group; + } + +} diff --git a/core/lib/Drupal/Core/Render/Plugin/TextSection/Code.php b/core/lib/Drupal/Core/Render/Plugin/TextSection/Code.php new file mode 100644 index 0000000..53a526d --- /dev/null +++ b/core/lib/Drupal/Core/Render/Plugin/TextSection/Code.php @@ -0,0 +1,26 @@ + '
',
+      '#suffix' => '
', + ] + parent::getInnerBuild(); + } + +} diff --git a/core/lib/Drupal/Core/Render/Plugin/TextSection/DescriptionName.php b/core/lib/Drupal/Core/Render/Plugin/TextSection/DescriptionName.php new file mode 100644 index 0000000..3a29c90 --- /dev/null +++ b/core/lib/Drupal/Core/Render/Plugin/TextSection/DescriptionName.php @@ -0,0 +1,53 @@ + '
', + '#suffix' => '
', + ] + parent::getInnerBuild(); + } + + /** + * {@inheritdoc} + */ + public function groupWith() { + return ['description_name', 'description_value']; + } + + /** + * {@inheritdoc} + */ + public function addToGroup(array $group) { + // Initialize the group, in case this is the first item. + $group += [ + '#prefix' => '
', + '#suffix' => '
', + 'list' => [], + ]; + + $group['list'][] = $this->getInnerBuild(); + + return $group; + } + +} diff --git a/core/lib/Drupal/Core/Render/Plugin/TextSection/DescriptionValue.php b/core/lib/Drupal/Core/Render/Plugin/TextSection/DescriptionValue.php new file mode 100644 index 0000000..047f08e --- /dev/null +++ b/core/lib/Drupal/Core/Render/Plugin/TextSection/DescriptionValue.php @@ -0,0 +1,53 @@ + '
', + '#suffix' => '
', + ] + parent::getInnerBuild(); + } + + /** + * {@inheritdoc} + */ + public function groupWith() { + return ['description_name', 'description_value']; + } + + /** + * {@inheritdoc} + */ + public function addToGroup(array $group) { + // Initialize the group, in case this is the first item. + $group += [ + '#prefix' => '
', + '#suffix' => '
', + 'list' => [], + ]; + + $group['list'][] = $this->getInnerBuild(); + + return $group; + } + +} diff --git a/core/lib/Drupal/Core/Render/Plugin/TextSection/Heading.php b/core/lib/Drupal/Core/Render/Plugin/TextSection/Heading.php new file mode 100644 index 0000000..7855aab --- /dev/null +++ b/core/lib/Drupal/Core/Render/Plugin/TextSection/Heading.php @@ -0,0 +1,26 @@ + '

', + '#suffix' => '

', + ] + parent::getInnerBuild(); + } + +} diff --git a/core/lib/Drupal/Core/Render/Plugin/TextSection/NumberedItem.php b/core/lib/Drupal/Core/Render/Plugin/TextSection/NumberedItem.php new file mode 100644 index 0000000..7701ee2 --- /dev/null +++ b/core/lib/Drupal/Core/Render/Plugin/TextSection/NumberedItem.php @@ -0,0 +1,39 @@ + 'item_list', + '#list_type' => 'ol', + '#items' => [], + ]; + + $group['#items'][] = $this->getInnerBuild(); + + return $group; + } + +} diff --git a/core/lib/Drupal/Core/Render/Plugin/TextSection/Paragraph.php b/core/lib/Drupal/Core/Render/Plugin/TextSection/Paragraph.php new file mode 100644 index 0000000..ee1d821 --- /dev/null +++ b/core/lib/Drupal/Core/Render/Plugin/TextSection/Paragraph.php @@ -0,0 +1,26 @@ + '

', + '#suffix' => '

', + ] + parent::getInnerBuild(); + } + +} diff --git a/core/lib/Drupal/Core/Render/Plugin/TextSection/SubHeading.php b/core/lib/Drupal/Core/Render/Plugin/TextSection/SubHeading.php new file mode 100644 index 0000000..626c00a --- /dev/null +++ b/core/lib/Drupal/Core/Render/Plugin/TextSection/SubHeading.php @@ -0,0 +1,26 @@ + '

', + '#suffix' => '

', + ] + parent::getInnerBuild(); + } + +} diff --git a/core/lib/Drupal/Core/Render/Plugin/TextSection/TextSectionBase.php b/core/lib/Drupal/Core/Render/Plugin/TextSection/TextSectionBase.php new file mode 100644 index 0000000..cff171d --- /dev/null +++ b/core/lib/Drupal/Core/Render/Plugin/TextSection/TextSectionBase.php @@ -0,0 +1,55 @@ + $text]; + } + $this->text = $text; + } + + /** + * {@inheritdoc} + */ + public function getInnerBuild() { + return $this->text; + } + + /** + * {@inheritdoc} + */ + public function groupWith() { + return []; + } + + /** + * {@inheritdoc} + */ + public function addToGroup(array $group) { + return []; + } + +} diff --git a/core/lib/Drupal/Core/Render/TextSectionManager.php b/core/lib/Drupal/Core/Render/TextSectionManager.php new file mode 100644 index 0000000..1c52772 --- /dev/null +++ b/core/lib/Drupal/Core/Render/TextSectionManager.php @@ -0,0 +1,95 @@ +alterInfo('text_section_info'); + $this->setCacheBackend($cache_backend, 'text_section_plugins'); + } + + /** + * {@inheritdoc} + */ + public function findDefinitions() { + $definitions = parent::findDefinitions(); + uasort($definitions, [$this, 'sortPlugins']); + return $definitions; + } + + /** + * Makes an options array for a select list of text section plugins. + * + * @return array + * Array whose keys are machine names of text section plugins, and whose + * values are the translated labels for the plugins, suitable for use as + * options for radios or select lists. + */ + public function getOptionsList() { + $plugins = $this->getDefinitions(); + $options = []; + foreach ($plugins as $plugin_id => $plugin_definition) { + $options[$plugin_id] = $plugin_definition['label']; + } + + return $options; + } + + /** + * Provides a uasort() callback for sorting plugin definitions. + * + * Sorts by weight first, and within the same weight, alphabetically by + * label. + * + * @param array $definition1 + * First plugin definition to compare. + * @param array $definition2 + * Second plugin definition to compare. + * + * @return int + * A number <1 if the first plugin should come first, >1 if the second + * should, and 0 if this cannot be determined. + */ + protected function sortPlugins($definition1, $definition2) { + if ($definition1['weight'] < $definition2['weight']) { + return -1; + } + if ($definition2['weight'] < $definition1['weight']) { + return 1; + } + if ($definition1['label'] < $definition2['label']) { + return -1; + } + if ($definition2['label'] < $definition1['label']) { + return 1; + } + return 0; + } + +} diff --git a/core/lib/Drupal/Core/Render/TextSectionPluginInterface.php b/core/lib/Drupal/Core/Render/TextSectionPluginInterface.php new file mode 100644 index 0000000..10449f0 --- /dev/null +++ b/core/lib/Drupal/Core/Render/TextSectionPluginInterface.php @@ -0,0 +1,77 @@ +' . t('Getting Started') . ''; - $output .= '

' . t('Follow these steps to set up and start using your website:') . '

'; - $output .= '
    '; - $output .= '
  1. ' . t('Configure your website Once logged in, visit the Administration page, where you may customize and configure all aspects of your website.', array(':admin' => \Drupal::url('system.admin'), ':config' => \Drupal::url('system.admin_config'))) . '
  2. '; - $output .= '
  3. ' . t('Enable additional functionality Next, visit the Extend page and enable modules that suit your specific needs. You can find additional modules at the Drupal.org modules page.', array(':modules' => \Drupal::url('system.modules_list'), ':download_modules' => 'https://www.drupal.org/project/modules')) . '
  4. '; - $output .= '
  5. ' . t('Customize your website design To change the "look and feel" of your website, visit the Appearance page. You may choose from one of the included themes or download additional themes from the Drupal.org themes page.', array(':themes' => \Drupal::url('system.themes_page'), ':download_themes' => 'https://www.drupal.org/project/themes')) . '
  6. '; + $sections = []; + $sections[] = [ + '#section_type' => 'heading', + '#text' => t('Getting Started'), + ]; + $sections[] = [ + '#section_type' => 'paragraph', + '#text' => t('Follow these steps to set up and start using your website:'), + ]; + $sections[] = [ + '#section_type' => 'numbered', + '#text' => t('Configure your website Once logged in, visit the Administration page, where you may customize and configure all aspects of your website.', array(':admin' => \Drupal::url('system.admin'), ':config' => \Drupal::url('system.admin_config'))), + ]; + $sections[] = [ + '#section_type' => 'numbered', + '#text' => t('Enable additional functionality Next, visit the Extend page and enable modules that suit your specific needs. You can find additional modules at the Drupal.org modules page.', array(':modules' => \Drupal::url('system.modules_list'), ':download_modules' => 'https://www.drupal.org/project/modules')), + ]; + $sections[] = [ + '#section_type' => 'numbered', + '#text' => t('Customize your website design To change the "look and feel" of your website, visit the Appearance page. You may choose from one of the included themes or download additional themes from the Drupal.org themes page.', array(':themes' => \Drupal::url('system.themes_page'), ':download_themes' => 'https://www.drupal.org/project/themes')), + ]; + // Display a link to the create content page if Node module is enabled. if (\Drupal::moduleHandler()->moduleExists('node')) { - $output .= '
  7. ' . t('Start posting content Finally, you may add new content to your website.', array(':content' => \Drupal::url('node.add_page'))) . '
  8. '; + $sections[] = [ + '#section_type' => 'numbered', + '#text' => t('Start posting content Finally, you may add new content to your website.', array(':content' => \Drupal::url('node.add_page'))), + ]; } - $output .= '
'; - $output .= '

' . t('For more information, refer to the help listed on this page or to the online documentation and support pages at drupal.org.', array(':docs' => 'https://www.drupal.org/documentation', ':support' => 'https://www.drupal.org/support', ':drupal' => 'https://www.drupal.org')) . '

'; - return ['#markup' => $output]; + $sections[] = [ + '#section_type' => 'paragraph', + '#text' => t('For more information, refer to the help listed on this page or to the online documentation and support pages at drupal.org.', array(':docs' => 'https://www.drupal.org/documentation', ':support' => 'https://www.drupal.org/support', ':drupal' => 'https://www.drupal.org')), + ]; + + $build = [ + '#type' => 'text_sections', + '#sections' => $sections, + ]; + + return [$build]; case 'help.page.help': - $output = ''; - $output .= '

' . t('About') . '

'; - $output .= '

' . t('The Help module generates Help reference pages to guide you through the use and configuration of modules, and provides a Help block with page-level help. The reference pages are a starting point for Drupal.org online documentation pages that contain more extensive and up-to-date information, are annotated with user-contributed comments, and serve as the definitive reference point for all Drupal documentation. For more information, see the online documentation for the Help module.', array(':help' => 'https://www.drupal.org/documentation/modules/help/', ':handbook' => 'https://www.drupal.org/documentation', ':help-page' => \Drupal::url('help.main'))) . '

'; - $output .= '

' . t('Uses') . '

'; - $output .= '
'; - $output .= '
' . t('Providing a help reference') . '
'; - $output .= '
' . t('The Help module displays explanations for using each module listed on the main Help reference page.', array(':help' => \Drupal::url('help.main'))) . '
'; - $output .= '
' . t('Providing page-specific help') . '
'; - $output .= '
' . t('Page-specific help text provided by modules is displayed in the Help block. This block can be placed and configured on the Block layout page.', array(':blocks' => (\Drupal::moduleHandler()->moduleExists('block')) ? \Drupal::url('block.admin_display') : '#')) . '
'; - $output .= '
'; - return ['#markup' => $output]; + $sections = []; + $sections[] = [ + '#section_type' => 'subheading', + '#text' => t('About'), + ]; + $sections[] = [ + '#section_type' => 'paragraph', + '#text' => t('The Help module generates Help reference pages to guide you through the use and configuration of modules, and provides a Help block with page-level help. The reference pages are a starting point for Drupal.org online documentation pages that contain more extensive and up-to-date information, are annotated with user-contributed comments, and serve as the definitive reference point for all Drupal documentation. For more information, see the online documentation for the Help module.', array(':help' => 'https://www.drupal.org/documentation/modules/help/', ':handbook' => 'https://www.drupal.org/documentation', ':help-page' => \Drupal::url('help.main'))), + ]; + $sections[] = [ + '#section_type' => 'subheading', + '#text' => t('Uses'), + ]; + $sections[] = [ + '#section_type' => 'description_name', + '#text' => t('Providing a help reference'), + ]; + $sections[] = [ + '#section_type' => 'description_value', + '#text' => t('The Help module displays explanations for using each module listed on the main Help reference page.', array(':help' => \Drupal::url('help.main'))), + ]; + $sections[] = [ + '#section_type' => 'description_name', + '#text' => t('Providing page-specific help'), + ]; + $sections[] = [ + '#section_type' => 'description_value', + '#text' => t('Page-specific help text provided by modules is displayed in the Help block. This block can be placed and configured on the Block layout page.', array(':blocks' => (\Drupal::moduleHandler()->moduleExists('block')) ? \Drupal::url('block.admin_display') : '#')), + ]; + + $build = [ + '#type' => 'text_sections', + '#sections' => $sections, + ]; + + return $build; } } diff --git a/core/tests/Drupal/KernelTests/Core/Render/TextSectionRenderingTest.php b/core/tests/Drupal/KernelTests/Core/Render/TextSectionRenderingTest.php new file mode 100644 index 0000000..1605f99 --- /dev/null +++ b/core/tests/Drupal/KernelTests/Core/Render/TextSectionRenderingTest.php @@ -0,0 +1,218 @@ + 'text_sections', + '#sections' => [ + [ + '#section_type' => 'heading', + '#text' => ['#markup' => 'First H2 header'], + ], + [ + '#section_type' => 'heading', + '#text' => 'Second H2 header', + ], + [ + '#section_type' => 'subheading', + '#text' => ['#markup' => 'First H3 sub-header'], + ], + [ + '#section_type' => 'subheading', + '#text' => 'Second H3 sub-header', + ], + [ + '#section_type' => 'paragraph', + '#text' => 'First paragraph', + ], + [ + '#section_type' => 'paragraph', + '#text' => ['#markup' => 'Second paragraph'], + ], + [ + '#section_type' => 'code', + '#text' => 'Some code', + ], + [ + '#section_type' => 'code', + '#text' => ['#markup' => 'More code'], + ], + [ + '#section_type' => 'bullet', + '#text' => ['#markup' => 'First list, first bullet'], + ], + [ + '#section_type' => 'bullet', + '#text' => 'First list, second bullet', + ], + [ + '#section_type' => 'numbered', + '#text' => ['#markup' => 'Numbered list, first item'], + ], + [ + '#section_type' => 'numbered', + '#text' => 'Numbered list, second item', + ], + [ + '#section_type' => 'description_name', + '#text' => 'DL list, first name', + ], + [ + '#section_type' => 'description_value', + '#text' => 'DL list, first value', + ], + [ + '#section_type' => 'description_name', + '#text' => ['#markup' => 'DL list, second name'], + ], + [ + '#section_type' => 'description_value', + '#text' => ['#markup' => 'DL list, second value'], + ], + ], + ]; + + // Pre-render the element into a more basic render array. + $result = TextSections::preRenderSections($element); + + // Compare to the expected result, but only the text_sections part. + $expected = [ + [ + '#prefix' => '

', + '#suffix' => '

', + '#markup' => 'First H2 header', + ], + [ + '#prefix' => '

', + '#suffix' => '

', + '#markup' => 'Second H2 header', + ], + [ + '#prefix' => '

', + '#suffix' => '

', + '#markup' => 'First H3 sub-header', + ], + [ + '#prefix' => '

', + '#suffix' => '

', + '#markup' => 'Second H3 sub-header', + ], + [ + '#prefix' => '

', + '#suffix' => '

', + '#markup' => 'First paragraph', + ], + [ + '#prefix' => '

', + '#suffix' => '

', + '#markup' => 'Second paragraph', + ], + [ + '#prefix' => '
',
+        '#suffix' => '
', + '#markup' => 'Some code', + ], + [ + '#prefix' => '
',
+        '#suffix' => '
', + '#markup' => 'More code', + ], + [ + '#theme' => 'item_list', + '#list_type' => 'ul', + '#items' => [ + ['#markup' => 'First list, first bullet'], + ['#markup' => 'First list, second bullet'], + ], + ], + [ + '#theme' => 'item_list', + '#list_type' => 'ol', + '#items' => [ + ['#markup' => 'Numbered list, first item'], + ['#markup' => 'Numbered list, second item'], + ], + ], + [ + '#prefix' => '
', + '#suffix' => '
', + 'list' => [ + [ + '#prefix' => '
', + '#suffix' => '
', + '#markup' => 'DL list, first name', + ], + [ + '#prefix' => '
', + '#suffix' => '
', + '#markup' => 'DL list, first value', + ], + [ + '#prefix' => '
', + '#suffix' => '
', + '#markup' => 'DL list, second name', + ], + [ + '#prefix' => '
', + '#suffix' => '
', + '#markup' => 'DL list, second value', + ], + ], + ]]; + + $this->assertTrue($result['text_sections'] == $expected, 'Pre-processing behaved as expected'); + } + + /** + * Tests the options list for text sections. + */ + public function testOptionsList() { + /** @var \Drupal\Core\Render\TextSectionManager $manager */ + $manager = \Drupal::service('plugin.manager.text_section'); + $options = $manager->getOptionsList(); + + // The sorting of the list should be by weight first, then alphabetically + // by label. + $expected = [ + // Weight is -100. + 'paragraph' => 'Paragraph', + // Weight is -20. + 'heading' => 'Heading', + // Weight is -19. + 'subheading' => 'Sub-heading', + // Weight is -10. + 'bullet' => 'Bullet list item', + 'numbered' => 'Numbered list item', + // Weight is 10. + 'description_name' => 'Description list name item', + // Weight is 11. + 'description_value' => 'Description list value item', + // Weight is 100. + 'code' => 'Code', + ]; + + $this->assertTrue($options == $expected, 'Options list was as expected'); + } + +}