diff --git a/core/modules/ckeditor/src/CKEditorPluginBase.php b/core/modules/ckeditor/src/CKEditorPluginBase.php
index af284fc71b..62d4a4d566 100644
--- a/core/modules/ckeditor/src/CKEditorPluginBase.php
+++ b/core/modules/ckeditor/src/CKEditorPluginBase.php
@@ -51,4 +51,33 @@ public function getLibraries(Editor $editor) {
     return [];
   }
 
+  /**
+   * CKEditor button template.
+   *
+   * @param string $name
+   *   Button name.
+   * @param string $direction
+   *   Language direction.
+   *
+   * @return array
+   *   Renderable array.
+   */
+  protected function buttonTemplate($name, $direction = 'ltr') {
+    // In the markup below, we mostly use the name (which may include spaces),
+    // but in one spot we use it as a CSS class, so strip spaces.
+    // Note: this uses str_replace() instead of Html::cleanCssIdentifier()
+    // because we must provide these class names exactly how CKEditor expects
+    // them in its library, which cleanCssIdentifier() does not do.
+    $class_name = str_replace(' ', '', $name);
+    return [
+      '#type' => 'inline_template',
+      '#template' => '<a href="#" class="cke-icon-only cke_{{ direction }}" role="button" title="{{ name }}" aria-label="{{ name }}"><span class="cke_button_icon cke_button__{{ classname }}_icon">{{ name }}</span></a>',
+      '#context' => [
+        'direction' => $direction,
+        'name' => $name,
+        'classname' => $class_name,
+      ],
+    ];
+  }
+
 }
diff --git a/core/modules/ckeditor/src/Plugin/CKEditorPlugin/DrupalSpecialChars.php b/core/modules/ckeditor/src/Plugin/CKEditorPlugin/DrupalSpecialChars.php
new file mode 100644
index 0000000000..3dc54b9861
--- /dev/null
+++ b/core/modules/ckeditor/src/Plugin/CKEditorPlugin/DrupalSpecialChars.php
@@ -0,0 +1,159 @@
+<?php
+
+namespace Drupal\ckeditor\Plugin\CKEditorPlugin;
+
+use Drupal\ckeditor\CKEditorPluginBase;
+use Drupal\ckeditor\CKEditorPluginConfigurableInterface;
+use Drupal\ckeditor\CKEditorPluginContextualInterface;
+use Drupal\Core\Form\FormStateInterface;
+use Drupal\editor\Entity\Editor;
+
+/**
+ * Defines the "specialChars" plugin.
+ *
+ * @CKEditorPlugin(
+ *   id = "specialchars",
+ *   label = @Translation("Special character"),
+ *   module = "ckeditor"
+ * )
+ */
+class DrupalSpecialChars extends CKEditorPluginBase implements CKEditorPluginConfigurableInterface, CKEditorPluginContextualInterface {
+
+  /**
+   * Default characters to use.
+   *
+   * Copied from CKEDITOR.config.specialChars in
+   * file core/assets/vendor/ckeditor/plugins/specialchar/plugin.js.
+   *
+   * @var array
+   */
+  public $defaultCharacters = [
+    '!', '&quot;', '#', '$', '%', '&amp;', "'", '(', ')', '*', '+', '-', '.', '/',
+    '0', '1', '2', '3', '4', '5', '6', '7', '8', '9', ':', ';',
+    '&lt;', '=', '&gt;', '?', '@',
+    'A', 'B', 'C', 'D', 'E', 'F', 'G', 'H', 'I', 'J', 'K', 'L', 'M', 'N', 'O',
+    'P', 'Q', 'R', 'S', 'T', 'U', 'V', 'W', 'X', 'Y', 'Z',
+    '[', ']', '^', '_', '`',
+    'a', 'b', 'c', 'd', 'e', 'f', 'g', 'h', 'i', 'j', 'k', 'l', 'm', 'n', 'o', 'p',
+    'q', 'r', 's', 't', 'u', 'v', 'w', 'x', 'y', 'z',
+    '{', '|', '}', '~',
+    '&euro;', '&lsquo;', '&rsquo;', '&ldquo;', '&rdquo;', '&ndash;', '&mdash;', '&iexcl;', '&cent;', '&pound;',
+    '&curren;', '&yen;', '&brvbar;', '&sect;', '&uml;', '&copy;', '&ordf;', '&laquo;', '&not;', '&reg;', '&macr;',
+    '&deg;', '&sup2;', '&sup3;', '&acute;', '&micro;', '&para;', '&middot;', '&cedil;', '&sup1;', '&ordm;', '&raquo;',
+    '&frac14;', '&frac12;', '&frac34;', '&iquest;', '&Agrave;', '&Aacute;', '&Acirc;', '&Atilde;', '&Auml;', '&Aring;',
+    '&AElig;', '&Ccedil;', '&Egrave;', '&Eacute;', '&Ecirc;', '&Euml;', '&Igrave;', '&Iacute;', '&Icirc;', '&Iuml;',
+    '&ETH;', '&Ntilde;', '&Ograve;', '&Oacute;', '&Ocirc;', '&Otilde;', '&Ouml;', '&times;', '&Oslash;', '&Ugrave;',
+    '&Uacute;', '&Ucirc;', '&Uuml;', '&Yacute;', '&THORN;', '&szlig;', '&agrave;', '&aacute;', '&acirc;', '&atilde;',
+    '&auml;', '&aring;', '&aelig;', '&ccedil;', '&egrave;', '&eacute;', '&ecirc;', '&euml;', '&igrave;', '&iacute;',
+    '&icirc;', '&iuml;', '&eth;', '&ntilde;', '&ograve;', '&oacute;', '&ocirc;', '&otilde;', '&ouml;', '&divide;',
+    '&oslash;', '&ugrave;', '&uacute;', '&ucirc;', '&uuml;', '&yacute;', '&thorn;', '&yuml;', '&OElig;', '&oelig;',
+    '&#372;', '&#374', '&#373', '&#375;', '&sbquo;', '&#8219;', '&bdquo;', '&hellip;', '&trade;', '&#9658;', '&bull;',
+    '&rarr;', '&rArr;', '&hArr;', '&diams;', '&asymp;',
+  ];
+
+  /**
+   * {@inheritdoc}
+   */
+  public function isInternal() {
+    return TRUE;
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function isEnabled(Editor $editor) {
+    $settings = $editor->getSettings();
+    foreach ($settings['toolbar']['rows'] as $row) {
+      foreach ($row as $group) {
+        foreach ($group['items'] as $button) {
+          if ($button === 'SpecialChar') {
+            return TRUE;
+          }
+        }
+      }
+    }
+
+    return FALSE;
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function getFile() {
+    return FALSE;
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function getLibraries(Editor $editor) {
+    return [];
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function getConfig(Editor $editor) {
+    // Defaults.
+    $config = ['characters' => '', 'replace' => FALSE];
+    $settings = $editor->getSettings();
+    if (isset($settings['plugins']['specialchars'])) {
+      $config = $settings['plugins']['specialchars'];
+    }
+
+    $characters = explode("\n", $config['characters']);
+    $characters = array_map('trim', $characters);
+    $characters = array_filter($characters, 'strlen');
+
+    // Not replace = append.
+    if (!$config['replace']) {
+      $characters = array_merge($this->defaultCharacters, $characters);
+    }
+
+    return [
+      'specialChars' => $characters,
+    ];
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function getButtons() {
+    return [
+      'SpecialChar' => [
+        'label' => $this->t('Character map'),
+        'image_alternative' => $this->buttonTemplate('special char'),
+        'image_alternative_rtl' => $this->buttonTemplate('special char', 'rtl'),
+      ],
+    ];
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function settingsForm(array $form, FormStateInterface $form_state, Editor $editor) {
+    // Defaults.
+    $config = ['characters' => '', 'replace' => FALSE];
+    $settings = $editor->getSettings();
+    if (isset($settings['plugins']['specialchars'])) {
+      $config = $settings['plugins']['specialchars'];
+    }
+
+    $form['characters'] = [
+      '#type' => 'textarea',
+      '#title' => $this->t('Special characters'),
+      '#descrption' => $this->t('One per line'),
+      '#default_value' => $config['characters'],
+    ];
+
+    $form['replace'] = [
+      '#type' => 'checkbox',
+      '#title' => $this->t('Replace default characters?'),
+      '#description' => $this->t('Leaving un-checked will append to default character list.'),
+      '#default_value' => $config['replace'],
+    ];
+
+    return $form;
+  }
+
+}
diff --git a/core/modules/ckeditor/src/Plugin/CKEditorPlugin/Internal.php b/core/modules/ckeditor/src/Plugin/CKEditorPlugin/Internal.php
index 9a04ce72a2..793d845c15 100644
--- a/core/modules/ckeditor/src/Plugin/CKEditorPlugin/Internal.php
+++ b/core/modules/ckeditor/src/Plugin/CKEditorPlugin/Internal.php
@@ -126,161 +126,137 @@ public function getConfig(Editor $editor) {
    * {@inheritdoc}
    */
   public function getButtons() {
-    $button = function($name, $direction = 'ltr') {
-      // In the markup below, we mostly use the name (which may include spaces),
-      // but in one spot we use it as a CSS class, so strip spaces.
-      // Note: this uses str_replace() instead of Html::cleanCssIdentifier()
-      // because we must provide these class names exactly how CKEditor expects
-      // them in its library, which cleanCssIdentifier() does not do.
-      $class_name = str_replace(' ', '', $name);
-      return [
-        '#type' => 'inline_template',
-        '#template' => '<a href="#" class="cke-icon-only cke_{{ direction }}" role="button" title="{{ name }}" aria-label="{{ name }}"><span class="cke_button_icon cke_button__{{ classname }}_icon">{{ name }}</span></a>',
-        '#context' => [
-          'direction' => $direction,
-          'name' => $name,
-          'classname' => $class_name,
-        ],
-      ];
-    };
-
     return [
       // "basicstyles" plugin.
       'Bold' => [
         'label' => $this->t('Bold'),
-        'image_alternative' => $button('bold'),
-        'image_alternative_rtl' => $button('bold', 'rtl'),
+        'image_alternative' => $this->buttonTemplate('bold'),
+        'image_alternative_rtl' => $this->buttonTemplate('bold', 'rtl'),
       ],
       'Italic' => [
         'label' => $this->t('Italic'),
-        'image_alternative' => $button('italic'),
-        'image_alternative_rtl' => $button('italic', 'rtl'),
+        'image_alternative' => $this->buttonTemplate('italic'),
+        'image_alternative_rtl' => $this->buttonTemplate('italic', 'rtl'),
       ],
       'Underline' => [
         'label' => $this->t('Underline'),
-        'image_alternative' => $button('underline'),
-        'image_alternative_rtl' => $button('underline', 'rtl'),
+        'image_alternative' => $this->buttonTemplate('underline'),
+        'image_alternative_rtl' => $this->buttonTemplate('underline', 'rtl'),
       ],
       'Strike' => [
         'label' => $this->t('Strike-through'),
-        'image_alternative' => $button('strike'),
-        'image_alternative_rtl' => $button('strike', 'rtl'),
+        'image_alternative' => $this->buttonTemplate('strike'),
+        'image_alternative_rtl' => $this->buttonTemplate('strike', 'rtl'),
       ],
       'Superscript' => [
         'label' => $this->t('Superscript'),
-        'image_alternative' => $button('super script'),
-        'image_alternative_rtl' => $button('super script', 'rtl'),
+        'image_alternative' => $this->buttonTemplate('super script'),
+        'image_alternative_rtl' => $this->buttonTemplate('super script', 'rtl'),
       ],
       'Subscript' => [
         'label' => $this->t('Subscript'),
-        'image_alternative' => $button('sub script'),
-        'image_alternative_rtl' => $button('sub script', 'rtl'),
+        'image_alternative' => $this->buttonTemplate('sub script'),
+        'image_alternative_rtl' => $this->buttonTemplate('sub script', 'rtl'),
       ],
       // "removeformat" plugin.
       'RemoveFormat' => [
         'label' => $this->t('Remove format'),
-        'image_alternative' => $button('remove format'),
-        'image_alternative_rtl' => $button('remove format', 'rtl'),
+        'image_alternative' => $this->buttonTemplate('remove format'),
+        'image_alternative_rtl' => $this->buttonTemplate('remove format', 'rtl'),
       ],
       // "justify" plugin.
       'JustifyLeft' => [
         'label' => $this->t('Align left'),
-        'image_alternative' => $button('justify left'),
-        'image_alternative_rtl' => $button('justify left', 'rtl'),
+        'image_alternative' => $this->buttonTemplate('justify left'),
+        'image_alternative_rtl' => $this->buttonTemplate('justify left', 'rtl'),
       ],
       'JustifyCenter' => [
         'label' => $this->t('Align center'),
-        'image_alternative' => $button('justify center'),
-        'image_alternative_rtl' => $button('justify center', 'rtl'),
+        'image_alternative' => $this->buttonTemplate('justify center'),
+        'image_alternative_rtl' => $this->buttonTemplate('justify center', 'rtl'),
       ],
       'JustifyRight' => [
         'label' => $this->t('Align right'),
-        'image_alternative' => $button('justify right'),
-        'image_alternative_rtl' => $button('justify right', 'rtl'),
+        'image_alternative' => $this->buttonTemplate('justify right'),
+        'image_alternative_rtl' => $this->buttonTemplate('justify right', 'rtl'),
       ],
       'JustifyBlock' => [
         'label' => $this->t('Justify'),
-        'image_alternative' => $button('justify block'),
-        'image_alternative_rtl' => $button('justify block', 'rtl'),
+        'image_alternative' => $this->buttonTemplate('justify block'),
+        'image_alternative_rtl' => $this->buttonTemplate('justify block', 'rtl'),
       ],
       // "list" plugin.
       'BulletedList' => [
         'label' => $this->t('Bullet list'),
-        'image_alternative' => $button('bulleted list'),
-        'image_alternative_rtl' => $button('bulleted list', 'rtl'),
+        'image_alternative' => $this->buttonTemplate('bulleted list'),
+        'image_alternative_rtl' => $this->buttonTemplate('bulleted list', 'rtl'),
       ],
       'NumberedList' => [
         'label' => $this->t('Numbered list'),
-        'image_alternative' => $button('numbered list'),
-        'image_alternative_rtl' => $button('numbered list', 'rtl'),
+        'image_alternative' => $this->buttonTemplate('numbered list'),
+        'image_alternative_rtl' => $this->buttonTemplate('numbered list', 'rtl'),
       ],
       // "indent" plugin.
       'Outdent' => [
         'label' => $this->t('Outdent'),
-        'image_alternative' => $button('outdent'),
-        'image_alternative_rtl' => $button('outdent', 'rtl'),
+        'image_alternative' => $this->buttonTemplate('outdent'),
+        'image_alternative_rtl' => $this->buttonTemplate('outdent', 'rtl'),
       ],
       'Indent' => [
         'label' => $this->t('Indent'),
-        'image_alternative' => $button('indent'),
-        'image_alternative_rtl' => $button('indent', 'rtl'),
+        'image_alternative' => $this->buttonTemplate('indent'),
+        'image_alternative_rtl' => $this->buttonTemplate('indent', 'rtl'),
       ],
       // "undo" plugin.
       'Undo' => [
         'label' => $this->t('Undo'),
-        'image_alternative' => $button('undo'),
-        'image_alternative_rtl' => $button('undo', 'rtl'),
+        'image_alternative' => $this->buttonTemplate('undo'),
+        'image_alternative_rtl' => $this->buttonTemplate('undo', 'rtl'),
       ],
       'Redo' => [
         'label' => $this->t('Redo'),
-        'image_alternative' => $button('redo'),
-        'image_alternative_rtl' => $button('redo', 'rtl'),
+        'image_alternative' => $this->buttonTemplate('redo'),
+        'image_alternative_rtl' => $this->buttonTemplate('redo', 'rtl'),
       ],
       // "blockquote" plugin.
       'Blockquote' => [
         'label' => $this->t('Blockquote'),
-        'image_alternative' => $button('blockquote'),
-        'image_alternative_rtl' => $button('blockquote', 'rtl'),
+        'image_alternative' => $this->buttonTemplate('blockquote'),
+        'image_alternative_rtl' => $this->buttonTemplate('blockquote', 'rtl'),
       ],
-      // "horizontalrule" plugin
+      // "horizontalrule" plugin.
       'HorizontalRule' => [
         'label' => $this->t('Horizontal rule'),
-        'image_alternative' => $button('horizontal rule'),
-        'image_alternative_rtl' => $button('horizontal rule', 'rtl'),
+        'image_alternative' => $this->buttonTemplate('horizontal rule'),
+        'image_alternative_rtl' => $this->buttonTemplate('horizontal rule', 'rtl'),
       ],
       // "clipboard" plugin.
       'Cut' => [
         'label' => $this->t('Cut'),
-        'image_alternative' => $button('cut'),
-        'image_alternative_rtl' => $button('cut', 'rtl'),
+        'image_alternative' => $this->buttonTemplate('cut'),
+        'image_alternative_rtl' => $this->buttonTemplate('cut', 'rtl'),
       ],
       'Copy' => [
         'label' => $this->t('Copy'),
-        'image_alternative' => $button('copy'),
-        'image_alternative_rtl' => $button('copy', 'rtl'),
+        'image_alternative' => $this->buttonTemplate('copy'),
+        'image_alternative_rtl' => $this->buttonTemplate('copy', 'rtl'),
       ],
       'Paste' => [
         'label' => $this->t('Paste'),
-        'image_alternative' => $button('paste'),
-        'image_alternative_rtl' => $button('paste', 'rtl'),
+        'image_alternative' => $this->buttonTemplate('paste'),
+        'image_alternative_rtl' => $this->buttonTemplate('paste', 'rtl'),
       ],
       // "pastetext" plugin.
       'PasteText' => [
         'label' => $this->t('Paste Text'),
-        'image_alternative' => $button('paste text'),
-        'image_alternative_rtl' => $button('paste text', 'rtl'),
+        'image_alternative' => $this->buttonTemplate('paste text'),
+        'image_alternative_rtl' => $this->buttonTemplate('paste text', 'rtl'),
       ],
       // "pastefromword" plugin.
       'PasteFromWord' => [
         'label' => $this->t('Paste from Word'),
-        'image_alternative' => $button('paste from word'),
-        'image_alternative_rtl' => $button('paste from word', 'rtl'),
-      ],
-      // "specialchar" plugin.
-      'SpecialChar' => [
-        'label' => $this->t('Character map'),
-        'image_alternative' => $button('special char'),
-        'image_alternative_rtl' => $button('special char', 'rtl'),
+        'image_alternative' => $this->buttonTemplate('paste from word'),
+        'image_alternative_rtl' => $this->buttonTemplate('paste from word', 'rtl'),
       ],
       'Format' => [
         'label' => $this->t('HTML block format'),
@@ -295,26 +271,26 @@ public function getButtons() {
       // "table" plugin.
       'Table' => [
         'label' => $this->t('Table'),
-        'image_alternative' => $button('table'),
-        'image_alternative_rtl' => $button('table', 'rtl'),
+        'image_alternative' => $this->buttonTemplate('table'),
+        'image_alternative_rtl' => $this->buttonTemplate('table', 'rtl'),
       ],
       // "showblocks" plugin.
       'ShowBlocks' => [
         'label' => $this->t('Show blocks'),
-        'image_alternative' => $button('show blocks'),
-        'image_alternative_rtl' => $button('show blocks', 'rtl'),
+        'image_alternative' => $this->buttonTemplate('show blocks'),
+        'image_alternative_rtl' => $this->buttonTemplate('show blocks', 'rtl'),
       ],
       // "sourcearea" plugin.
       'Source' => [
         'label' => $this->t('Source code'),
-        'image_alternative' => $button('source'),
-        'image_alternative_rtl' => $button('source', 'rtl'),
+        'image_alternative' => $this->buttonTemplate('source'),
+        'image_alternative_rtl' => $this->buttonTemplate('source', 'rtl'),
       ],
       // "maximize" plugin.
       'Maximize' => [
         'label' => $this->t('Maximize'),
-        'image_alternative' => $button('maximize'),
-        'image_alternative_rtl' => $button('maximize', 'rtl'),
+        'image_alternative' => $this->buttonTemplate('maximize'),
+        'image_alternative_rtl' => $this->buttonTemplate('maximize', 'rtl'),
       ],
       // No plugin, separator "button" for toolbar builder UI use only.
       '-' => [
