diff --git a/wagtail/tests/testapp/wagtail_hooks.py b/wagtail/tests/testapp/wagtail_hooks.py index 81989c0913..d82c73b41f 100644 --- a/wagtail/tests/testapp/wagtail_hooks.py +++ b/wagtail/tests/testapp/wagtail_hooks.py @@ -90,3 +90,11 @@ def polite_pages_only(parent_page, pages, request): pages = pages.filter(slug__startswith='hello') return pages + + +# register 'blockquote' as a rich text feature supported by a hallo.js plugin +@hooks.register('register_rich_text_features') +def register_blockquote_feature(features): + features.register_editor_plugin( + 'hallo', 'blockquote', {'plugin_name': 'halloblockquote'} + ) diff --git a/wagtail/wagtailcore/rich_text.py b/wagtail/wagtailcore/rich_text.py index 22df18f7bf..bf336ccf8f 100644 --- a/wagtail/wagtailcore/rich_text.py +++ b/wagtail/wagtailcore/rich_text.py @@ -202,3 +202,46 @@ class RichText(object): def __bool__(self): return bool(self.source) __nonzero__ = __bool__ + + +class FeatureRegistry(object): + """ + A central store of information about optional features that can be enabled in rich text + editors by passing a ``features`` list to the RichTextField, such as how to + whitelist / convert HTML tags, and how to enable the feature on various editors. + + This information may come from diverse sources - for example, wagtailimages might define + an 'images' feature and a hallo.js plugin for it, while a third-party module might + define a TinyMCE plugin for the same feature. The information is therefore collected into + this registry via the 'register_rich_text_features' hook. + """ + def __init__(self): + # Has the register_rich_text_features hook been run for this registry? + self.has_scanned_for_features = False + + # a dict of dicts, one for each editor (hallo.js, TinyMCE etc); each dict is a mapping + # of feature names to 'plugin' objects that define how to implement that feature + # (e.g. paths to JS files to import). The API of that plugin object is not defined + # here, and is specific to each editor. + self.plugins_by_editor = {} + + def _scan_for_features(self): + for fn in hooks.get_hooks('register_rich_text_features'): + fn(self) + self.has_scanned_for_features = True + + def register_editor_plugin(self, editor_name, feature_name, plugin): + plugins = self.plugins_by_editor.setdefault(editor_name, {}) + plugins[feature_name] = plugin + + def get_editor_plugin(self, editor_name, feature_name): + if not self.has_scanned_for_features: + self._scan_for_features() + + try: + return self.plugins_by_editor[editor_name][feature_name] + except KeyError: + return None + + +features = FeatureRegistry() diff --git a/wagtail/wagtailcore/tests/test_rich_text.py b/wagtail/wagtailcore/tests/test_rich_text.py index d968ee4f25..68c328f54b 100644 --- a/wagtail/wagtailcore/tests/test_rich_text.py +++ b/wagtail/wagtailcore/tests/test_rich_text.py @@ -6,7 +6,7 @@ from mock import patch from wagtail.wagtailcore.models import Page from wagtail.wagtailcore.rich_text import ( - DbWhitelister, PageLinkHandler, RichText, expand_db_html, extract_attrs) + DbWhitelister, FeatureRegistry, PageLinkHandler, RichText, expand_db_html, extract_attrs) class TestPageLinkHandler(TestCase): @@ -144,3 +144,21 @@ class TestRichTextValue(TestCase): value = RichText('

wagtail

') self.assertTrue(value) + + +class TestFeatureRegistry(TestCase): + def test_register_rich_text_features_hook(self): + # testapp/wagtail_hooks.py defines a 'blockquote' rich text feature with a hallo.js + # plugin, via the register_rich_text_features hook; test that we can retrieve it here + features = FeatureRegistry() + blockquote = features.get_editor_plugin('hallo', 'blockquote') + self.assertEqual(blockquote['plugin_name'], 'halloblockquote') + + def test_missing_editor_plugin_returns_none(self): + features = FeatureRegistry() + self.assertIsNone( + features.get_editor_plugin('made_up_editor', 'blockquote') + ) + self.assertIsNone( + features.get_editor_plugin('hallo', 'made_up_feature') + )