diff --git a/last_commit.txt b/last_commit.txt index bef74f0ed1..12c0b4872f 100644 --- a/last_commit.txt +++ b/last_commit.txt @@ -1,241 +1,644 @@ -Repository: Products.CMFPlone +Repository: plone.outputfilters Branch: refs/heads/master -Date: 2022-05-17T12:08:36+02:00 +Date: 2022-04-19T17:22:02+03:00 Author: MrTango (MrTango) -Commit: https://github.com/plone/Products.CMFPlone/commit/5909d9f30e7cfc56cd967480f43573975b5d18bd +Commit: https://github.com/plone/plone.outputfilters/commit/d501f93529105e08af79dc7637a4032325273531 -tinymce pat settings from image_scales to image_srcsets +Add image_srcset output filter Files changed: -A news/3477.feature -M Products/CMFPlone/patterns/settings.py +A news/49.feature +A plone/outputfilters/filters/image_srcset.py +M plone/outputfilters/filters/configure.zcml +M plone/outputfilters/filters/resolveuid_and_caption.py +M plone/outputfilters/tests/test_resolveuid_and_caption.py -b'diff --git a/Products/CMFPlone/patterns/settings.py b/Products/CMFPlone/patterns/settings.py\nindex 2d63b3ef69..8f5afc81ab 100644\n--- a/Products/CMFPlone/patterns/settings.py\n+++ b/Products/CMFPlone/patterns/settings.py\n@@ -7,6 +7,7 @@\n from plone.registry.interfaces import IRegistry\n from plone.uuid.interfaces import IUUID\n from Products.CMFCore.interfaces._content import IFolderish\n+from plone.base.interfaces import IImagingSchema\n from plone.base.interfaces import ILinkSchema\n from plone.base.interfaces import IPatternsSettings\n from plone.base.interfaces import IPloneSiteRoot\n@@ -71,12 +72,10 @@ def mark_special_links(self):\n return result\n \n @property\n- def image_scales(self):\n- factory = getUtility(IVocabularyFactory, "plone.app.vocabularies.ImagesScales")\n- vocabulary = factory(self.context)\n- ret = [{"title": translate(it.title), "value": it.value} for it in vocabulary]\n- ret = sorted(ret, key=lambda it: it["title"])\n- return json.dumps(ret)\n+ def image_srcsets(self):\n+ registry = getUtility(IRegistry)\n+ settings = registry.forInterface(IImagingSchema, prefix="plone", check=False)\n+ return settings.image_srcsets\n \n def tinymce(self):\n """\n@@ -129,7 +128,7 @@ def tinymce(self):\n configuration = {\n "base_url": self.context.absolute_url(),\n "imageTypes": image_types,\n- "imageScales": self.image_scales,\n+ "imageSrcsets": self.image_srcsets,\n "linkAttribute": "UID",\n # This is for loading the languages on tinymce\n "loadingBaseUrl": "{}/++plone++static/components/tinymce-builded/"\ndiff --git a/news/3477.feature b/news/3477.feature\nnew file mode 100644\nindex 0000000000..78e99ea9a1\n--- /dev/null\n+++ b/news/3477.feature\n@@ -0,0 +1 @@\n+Add image srcset\'s configuration to TinyMCE pattern settings [MrTango]\n\\ No newline at end of file\n' +b'diff --git a/news/49.feature b/news/49.feature\nnew file mode 100644\nindex 0000000..ce4f04b\n--- /dev/null\n+++ b/news/49.feature\n@@ -0,0 +1 @@\n+Add image_srcset output filter, to convert IMG tags into PICTURE tags with multiple source definitions as define in imaging control panel [MrTango]\n\\ No newline at end of file\ndiff --git a/plone/outputfilters/filters/configure.zcml b/plone/outputfilters/filters/configure.zcml\nindex 19ef066..0899ce7 100644\n--- a/plone/outputfilters/filters/configure.zcml\n+++ b/plone/outputfilters/filters/configure.zcml\n@@ -10,6 +10,13 @@\n factory=".resolveuid_and_caption.ResolveUIDAndCaptionFilter"\n />\n \n+ \n+\n \n \n+ \n+ \n+ \n+ \n+ \n+ \n+ \n+ """\n+\n+ order = 700\n+\n+ def _shorttag_replace(self, match):\n+ tag = match.group(1)\n+ if tag in self.singleton_tags:\n+ return "<" + tag + " />"\n+ else:\n+ return "<" + tag + ">"\n+\n+ def is_enabled(self):\n+ if self.context is None:\n+ return False\n+ else:\n+ return True\n+\n+ def __init__(self, context=None, request=None):\n+ self.current_status = None\n+ self.context = context\n+ self.request = request\n+\n+ def __call__(self, data):\n+ data = re.sub(r"<([^<>\\s]+?)\\s*/>", self._shorttag_replace, data)\n+ soup = BeautifulSoup(safe_nativestring(data), "html.parser")\n+ self.image_srcsets = self.image_srcsets()\n+\n+ for elem in soup.find_all("img"):\n+ srcset_name = elem.attrs.get("data-srcset", "")\n+ if not srcset_name:\n+ continue\n+ elem.replace_with(self.convert_to_srcset(srcset_name, elem, soup))\n+ return str(soup)\n+\n+ def image_srcsets(self):\n+ registry = getUtility(IRegistry)\n+ settings = registry.forInterface(IImagingSchema, prefix="plone", check=False)\n+ return settings.image_srcsets\n+\n+ def convert_to_srcset(self, srcset_name, elem, soup):\n+ """Converts the element to a srcset definition\n+ """\n+ srcset_config = self.image_srcsets.get(srcset_name)\n+ sourceset = srcset_config.get(\'sourceset\')\n+ if not sourceset:\n+ return elem\n+ src = elem.attrs.get("src")\n+ picture_tag = soup.new_tag("picture")\n+ for i, source in enumerate(sourceset):\n+ scale = source[\'scale\']\n+ media = source.get(\'media\')\n+ title = elem.attrs.get(\'title\')\n+ alt = elem.attrs.get(\'alt\')\n+ klass = elem.attrs.get(\'class\')\n+ if i == len(sourceset) - 1:\n+ source_tag = soup.new_tag("img", src=self.update_src_scale(src=src, scale=scale))\n+ else:\n+ # TODO guess type:\n+ source_tag = soup.new_tag("source", srcset=self.update_src_scale(src=src, scale=scale))\n+ source_tag["loading"] = "lazy"\n+ if media:\n+ source_tag["media"] = media\n+ if title:\n+ source_tag["title"] = title\n+ if alt:\n+ source_tag["alt"] = alt\n+ if klass:\n+ source_tag["class"] = klass\n+ picture_tag.append(source_tag)\n+ return picture_tag\n+\n+ def update_src_scale(self, src, scale):\n+ parts = src.split("/")\n+ return "/".join(parts[:-1]) + "/{}".format(scale)\n\\ No newline at end of file\ndiff --git a/plone/outputfilters/filters/resolveuid_and_caption.py b/plone/outputfilters/filters/resolveuid_and_caption.py\nindex 935c926..4773bb1 100644\n--- a/plone/outputfilters/filters/resolveuid_and_caption.py\n+++ b/plone/outputfilters/filters/resolveuid_and_caption.py\n@@ -159,8 +159,9 @@ def __call__(self, data):\n and not href.startswith(\'#\'):\n attributes[\'href\'] = self._render_resolveuid(href)\n for elem in soup.find_all([\'source\', \'img\']):\n- # SOURCE is used for video and audio.\n- # SRCSET specified multiple images (see below).\n+ # handles srcset attributes, not src\n+ # parent of SOURCE is picture here.\n+ # SRCSET on source/img specifies one or more images (see below).\n attributes = elem.attrs\n srcset = attributes.get(\'srcset\')\n if not srcset:\n@@ -169,10 +170,11 @@ def __call__(self, data):\n # [(src1, 480w), (src2, 360w)]\n srcs = [src.strip().split() for src in srcset.strip().split(\',\') if src.strip()]\n for idx, elm in enumerate(srcs):\n- srcs[idx][0] = self._render_resolveuid(elm[0])\n+ image, fullimage, src, description = self.resolve_image(elm[0])\n+ srcs[idx][0] = src\n attributes[\'srcset\'] = \',\'.join(\' \'.join(src) for src in srcs)\n for elem in soup.find_all([\'source\', \'iframe\', \'audio\', \'video\']):\n- # SOURCE is used for video and audio.\n+ # parent of SOURCE is video or audio here.\n # AUDIO/VIDEO can also have src attribute.\n # IFRAME is used to embed PDFs.\n attributes = elem.attrs\n@@ -181,6 +183,7 @@ def __call__(self, data):\n continue\n attributes[\'src\'] = self._render_resolveuid(src)\n for elem in soup.find_all(\'img\'):\n+ # handles src attribute\n attributes = elem.attrs\n src = attributes.get(\'src\', \'\')\n image, fullimage, src, description = self.resolve_image(src)\n@@ -325,7 +328,7 @@ def handle_captioned_image(self, attributes, image, fullimage,\n elem, caption):\n """Handle captioned image.\n \n- The img element is replaced by a definition list\n+ The img element is replaced by figure\n as created by the template ../browser/captioned_image.pt\n """\n klass = \' \'.join(attributes[\'class\'])\ndiff --git a/plone/outputfilters/tests/test_resolveuid_and_caption.py b/plone/outputfilters/tests/test_resolveuid_and_caption.py\nindex ec748f5..cc9a971 100644\n--- a/plone/outputfilters/tests/test_resolveuid_and_caption.py\n+++ b/plone/outputfilters/tests/test_resolveuid_and_caption.py\n@@ -115,6 +115,71 @@ def test_parsing_minimal(self):\n res = self.parser(text)\n self.assertEqual(text, str(res))\n \n+ def test_parsing_long_doc(self):\n+ text = """
\n+

Welcome!

\n+

Learn more about Plone

\n+
\n+

If you\'re seeing this instead of the web site you were expecting, the owner of this web site has just installed Plone. Do not contact the Plone Team or the Plone support channels about this.

\n+

\n+

Get started

\n+

Before you start exploring your newly created Plone site, please do the following:

\n+
    \n+
  1. Make sure you are logged in as an admin/manager user. (You should have a Site Setup entry in the user menu)
  2. \n+
  3. Set up your mail server. (Plone needs a valid SMTP server to verify users and send out password reminders)
  4. \n+
  5. Decide what security level you want on your site. (Allow self registration, password policies, etc)
  6. \n+
\n+

Get comfortable

\n+

After that, we suggest you do one or more of the following:

\n+\n+

\n+

Make it your own

\n+

Plone has a lot of different settings that can be used to make it do what you want it to. Some examples:

\n+\n+

Tell us how you use it

\n+

Are you doing something interesting with Plone? Big site deployments, interesting use cases? Do you have a company that delivers Plone-based solutions?

\n+\n+

Find out more about Plone

\n+

Plone is a powerful content management system built on a rock-solid application stack written using the Python programming language. More about these technologies:

\n+\n+

\n+

Support the Plone Foundation

\n+

Plone is made possible only through the efforts of thousands of dedicated individuals and hundreds of companies. The Plone Foundation:

\n+\n+

Thanks for using our product; we hope you like it!

\n+

\xe2\x80\x94The Plone Team

\n+ """.format(uid=self.UID)\n+ import time\n+ startTime = time.time()\n+ res = self.parser(text)\n+ executionTime = (time.time() - startTime)\n+ print(executionTime)\n+ self.assertTrue(res)\n+\n def test_parsing_preserves_newlines(self):\n # Test if it preserves newlines which should not be filtered out\n text = """
This is line 1\n'
 
-Repository: Products.CMFPlone
+Repository: plone.outputfilters
 
 
 Branch: refs/heads/master
-Date: 2022-05-17T12:08:36+02:00
+Date: 2022-04-20T11:49:18+03:00
 Author: MrTango (MrTango) 
-Commit: https://github.com/plone/Products.CMFPlone/commit/15ba17e25dc9f932071a38c816bbbc83233799b0
+Commit: https://github.com/plone/plone.outputfilters/commit/b190b71a2a06138625d36ffffc8fb006faad63a9
 
-filter out srcsets with hideInEditor set to true
+Fix resolve_uid_and_caption tests
 
 Files changed:
-M Products/CMFPlone/patterns/settings.py
+M plone/outputfilters/tests/test_resolveuid_and_caption.py
 
-b'diff --git a/Products/CMFPlone/patterns/settings.py b/Products/CMFPlone/patterns/settings.py\nindex 8f5afc81ab..96b7210ac3 100644\n--- a/Products/CMFPlone/patterns/settings.py\n+++ b/Products/CMFPlone/patterns/settings.py\n@@ -75,7 +75,13 @@ def mark_special_links(self):\n     def image_srcsets(self):\n         registry = getUtility(IRegistry)\n         settings = registry.forInterface(IImagingSchema, prefix="plone", check=False)\n-        return settings.image_srcsets\n+        editor_srcsets = {}\n+        for k, srcset in settings.image_srcsets.items():\n+            hide_in_editor = srcset.get("hideInEditor")\n+            if hide_in_editor:\n+                continue\n+            editor_srcsets[k] = srcset\n+        return editor_srcsets\n \n     def tinymce(self):\n         """\n'
+b'diff --git a/plone/outputfilters/tests/test_resolveuid_and_caption.py b/plone/outputfilters/tests/test_resolveuid_and_caption.py\nindex cc9a971..cb011bd 100644\n--- a/plone/outputfilters/tests/test_resolveuid_and_caption.py\n+++ b/plone/outputfilters/tests/test_resolveuid_and_caption.py\n@@ -78,6 +78,8 @@ def _assertTransformsTo(self, input, expected):\n         out = self.parser(input)\n         normalized_out = normalize_html(out)\n         normalized_expected = normalize_html(expected)\n+        # print("e: {}".format(normalized_expected))\n+        # print("o: {}".format(normalized_out))\n         try:\n             self.assertTrue(_ellipsis_match(normalized_expected,\n                                             normalized_out))\n@@ -177,7 +179,7 @@ def test_parsing_long_doc(self):\n         startTime = time.time()\n         res = self.parser(text)\n         executionTime = (time.time() - startTime)\n-        print(executionTime)\n+        print("\\n\\nresolve_uid_and_caption parsing time: {}\\n".format(executionTime))\n         self.assertTrue(res)\n \n     def test_parsing_preserves_newlines(self):\n@@ -429,7 +431,7 @@ def test_image_captioning_resolveuid_no_scale(self):\n     def test_image_captioning_resolveuid_with_srcset_and_src(self):\n         text_in = """""" % (self.UID, self.UID, self.UID)\n         text_out = """
\n-My caption\n+My caption\n
My caption
\n
"""\n self._assertTransformsTo(text_in, text_out)\n' -Repository: Products.CMFPlone +Repository: plone.outputfilters Branch: refs/heads/master -Date: 2022-05-17T12:08:36+02:00 +Date: 2022-04-20T11:49:49+03:00 Author: MrTango (MrTango) -Commit: https://github.com/plone/Products.CMFPlone/commit/2b28f0ac610bf11427b729ba370443b73270363d +Commit: https://github.com/plone/plone.outputfilters/commit/e7005162bfefc164fbb67ac3bdfe58437053779c -add imageCaptioningEnabled param to TinyMCE settings +prevent image_srcset from breaking when srcset config is missing, add tests Files changed: -M Products/CMFPlone/patterns/settings.py +A plone/outputfilters/tests/test_image_srcset.py +M plone/outputfilters/filters/image_srcset.py -b'diff --git a/Products/CMFPlone/patterns/settings.py b/Products/CMFPlone/patterns/settings.py\nindex 96b7210ac3..cb7a236ead 100644\n--- a/Products/CMFPlone/patterns/settings.py\n+++ b/Products/CMFPlone/patterns/settings.py\n@@ -83,6 +83,12 @@ def image_srcsets(self):\n editor_srcsets[k] = srcset\n return editor_srcsets\n \n+ @property\n+ def image_captioning(self):\n+ registry = getUtility(IRegistry)\n+ settings = registry.forInterface(IImagingSchema, prefix="plone", check=False)\n+ return settings.image_captioning\n+\n def tinymce(self):\n """\n data-pat-tinymce : JSON.stringify({\n@@ -135,6 +141,7 @@ def tinymce(self):\n "base_url": self.context.absolute_url(),\n "imageTypes": image_types,\n "imageSrcsets": self.image_srcsets,\n+ "imageCaptioningEnabled": self.image_captioning,\n "linkAttribute": "UID",\n # This is for loading the languages on tinymce\n "loadingBaseUrl": "{}/++plone++static/components/tinymce-builded/"\n' +b'diff --git a/plone/outputfilters/filters/image_srcset.py b/plone/outputfilters/filters/image_srcset.py\nindex 059cfd4..ba289c9 100644\n--- a/plone/outputfilters/filters/image_srcset.py\n+++ b/plone/outputfilters/filters/image_srcset.py\n@@ -1,3 +1,4 @@\n+import logging\n import re\n \n from bs4 import BeautifulSoup\n@@ -8,6 +9,8 @@\n from zope.component import getUtility\n from zope.interface import implementer\n \n+logger = logging.getLogger("plone.outputfilter.image_srcset")\n+\n \n @implementer(IFilter)\n class ImageSrcsetFilter(object):\n@@ -47,10 +50,15 @@ def __init__(self, context=None, request=None):\n self.context = context\n self.request = request\n \n+ @property\n+ def image_srcsets(self):\n+ registry = getUtility(IRegistry)\n+ settings = registry.forInterface(IImagingSchema, prefix="plone", check=False)\n+ return settings.image_srcsets\n+\n def __call__(self, data):\n data = re.sub(r"<([^<>\\s]+?)\\s*/>", self._shorttag_replace, data)\n soup = BeautifulSoup(safe_nativestring(data), "html.parser")\n- self.image_srcsets = self.image_srcsets()\n \n for elem in soup.find_all("img"):\n srcset_name = elem.attrs.get("data-srcset", "")\n@@ -59,31 +67,36 @@ def __call__(self, data):\n elem.replace_with(self.convert_to_srcset(srcset_name, elem, soup))\n return str(soup)\n \n- def image_srcsets(self):\n- registry = getUtility(IRegistry)\n- settings = registry.forInterface(IImagingSchema, prefix="plone", check=False)\n- return settings.image_srcsets\n-\n def convert_to_srcset(self, srcset_name, elem, soup):\n- """Converts the element to a srcset definition\n- """\n+ """Converts the element to a srcset definition"""\n srcset_config = self.image_srcsets.get(srcset_name)\n- sourceset = srcset_config.get(\'sourceset\')\n+ if not srcset_config:\n+ logger.warn(\n+ "Could not find the given srcset_name {0}, leave tag untouched!".format(\n+ srcset_name\n+ )\n+ )\n+ return elem\n+ sourceset = srcset_config.get("sourceset")\n if not sourceset:\n return elem\n src = elem.attrs.get("src")\n picture_tag = soup.new_tag("picture")\n for i, source in enumerate(sourceset):\n- scale = source[\'scale\']\n- media = source.get(\'media\')\n- title = elem.attrs.get(\'title\')\n- alt = elem.attrs.get(\'alt\')\n- klass = elem.attrs.get(\'class\')\n+ scale = source["scale"]\n+ media = source.get("media")\n+ title = elem.attrs.get("title")\n+ alt = elem.attrs.get("alt")\n+ klass = elem.attrs.get("class")\n if i == len(sourceset) - 1:\n- source_tag = soup.new_tag("img", src=self.update_src_scale(src=src, scale=scale))\n+ source_tag = soup.new_tag(\n+ "img", src=self.update_src_scale(src=src, scale=scale)\n+ )\n else:\n # TODO guess type:\n- source_tag = soup.new_tag("source", srcset=self.update_src_scale(src=src, scale=scale))\n+ source_tag = soup.new_tag(\n+ "source", srcset=self.update_src_scale(src=src, scale=scale)\n+ )\n source_tag["loading"] = "lazy"\n if media:\n source_tag["media"] = media\n@@ -98,4 +111,4 @@ def convert_to_srcset(self, srcset_name, elem, soup):\n \n def update_src_scale(self, src, scale):\n parts = src.split("/")\n- return "/".join(parts[:-1]) + "/{}".format(scale)\n\\ No newline at end of file\n+ return "/".join(parts[:-1]) + "/{}".format(scale)\ndiff --git a/plone/outputfilters/tests/test_image_srcset.py b/plone/outputfilters/tests/test_image_srcset.py\nnew file mode 100644\nindex 0000000..0d56a7a\n--- /dev/null\n+++ b/plone/outputfilters/tests/test_image_srcset.py\n@@ -0,0 +1,198 @@\n+# -*- coding: utf-8 -*-\n+from doctest import _ellipsis_match\n+from doctest import OutputChecker\n+from doctest import REPORT_NDIFF\n+from os.path import abspath\n+from os.path import dirname\n+from os.path import join\n+from plone.app.testing import setRoles\n+from plone.app.testing import TEST_USER_ID\n+from plone.app.testing.bbb import PloneTestCase\n+from plone.namedfile.file import NamedBlobImage\n+from plone.namedfile.file import NamedImage\n+from plone.namedfile.tests.test_scaling import DummyContent as NFDummyContent\n+from plone.outputfilters.filters.image_srcset import ImageSrcsetFilter\n+from plone.outputfilters.testing import PLONE_OUTPUTFILTERS_FUNCTIONAL_TESTING\n+from Products.PortalTransforms.tests.utils import normalize_html\n+\n+\n+PREFIX = abspath(dirname(__file__))\n+\n+\n+def dummy_image():\n+ filename = join(PREFIX, u\'image.jpg\')\n+ data = None\n+ with open(filename, \'rb\') as fd:\n+ data = fd.read()\n+ fd.close()\n+ return NamedBlobImage(data=data, filename=filename)\n+\n+\n+class ImageSrcsetFilterIntegrationTestCase(PloneTestCase):\n+\n+ layer = PLONE_OUTPUTFILTERS_FUNCTIONAL_TESTING\n+\n+ image_id = \'image.jpg\'\n+\n+ def _makeParser(self, **kw):\n+ parser = ImageSrcsetFilter(context=self.portal)\n+ for k, v in kw.items():\n+ setattr(parser, k, v)\n+ return parser\n+\n+ def _makeDummyContent(self):\n+ from OFS.SimpleItem import SimpleItem\n+\n+ class DummyContent(SimpleItem):\n+\n+ def __init__(self, id):\n+ self.id = id\n+\n+ def UID(self):\n+ return \'foo\'\n+\n+ allowedRolesAndUsers = (\'Anonymous\',)\n+\n+ class DummyContent2(NFDummyContent):\n+ id = __name__ = \'foo2\'\n+ title = u\'Sch\xc3\xb6nes Bild\'\n+\n+ def UID(self):\n+ return \'foo2\'\n+\n+ dummy = DummyContent(\'foo\')\n+ self.portal._setObject(\'foo\', dummy)\n+ self.portal.portal_catalog.catalog_object(self.portal.foo)\n+\n+ dummy2 = DummyContent2(\'foo2\')\n+ with open(join(PREFIX, self.image_id), \'rb\') as fd:\n+ data = fd.read()\n+ fd.close()\n+ dummy2.image = NamedImage(data, \'image/jpeg\', u\'image.jpeg\')\n+ self.portal._setObject(\'foo2\', dummy2)\n+ self.portal.portal_catalog.catalog_object(self.portal.foo2)\n+\n+ def _assertTransformsTo(self, input, expected):\n+ # compare two chunks of HTML ignoring whitespace differences,\n+ # and with a useful diff on failure\n+ out = self.parser(input)\n+ normalized_out = normalize_html(out)\n+ normalized_expected = normalize_html(expected)\n+ # print("e: {}".format(normalized_expected))\n+ # print("o: {}".format(normalized_out))\n+ try:\n+ self.assertTrue(_ellipsis_match(normalized_expected,\n+ normalized_out))\n+ except AssertionError:\n+ class wrapper(object):\n+ want = expected\n+ raise AssertionError(self.outputchecker.output_difference(\n+ wrapper, out, REPORT_NDIFF))\n+\n+ def afterSetUp(self):\n+ # create an image and record its UID\n+ setRoles(self.portal, TEST_USER_ID, [\'Manager\'])\n+\n+ if self.image_id not in self.portal:\n+ self.portal.invokeFactory(\n+ \'Image\', id=self.image_id, title=\'Image\')\n+ image = self.portal[self.image_id]\n+ image.setDescription(\'My caption\')\n+ image.image = dummy_image()\n+ image.reindexObject()\n+ self.UID = image.UID()\n+ self.parser = self._makeParser(captioned_images=True,\n+ resolve_uids=True)\n+ assert self.parser.is_enabled()\n+\n+ self.outputchecker = OutputChecker()\n+\n+ def beforeTearDown(self):\n+ self.login()\n+ setRoles(self.portal, TEST_USER_ID, [\'Manager\'])\n+ del self.portal[self.image_id]\n+\n+ def test_parsing_minimal(self):\n+ text = \'
Some simple text.
\'\n+ res = self.parser(text)\n+ self.assertEqual(text, str(res))\n+\n+ def test_parsing_long_doc(self):\n+ text = """

Welcome!

\n+

If you\'re seeing this instead of the web site you were expecting, the owner of this web site has just installed Plone. Do not contact the Plone Team or the Plone support channels about this.

\n+

\n+

Get started

\n+

Before you start exploring your newly created Plone site, please do the following:

\n+
    \n+
  1. Make sure you are logged in as an admin/manager user. (You should have a Site Setup entry in the user menu)
  2. \n+
\n+

Get comfortable

\n+

After that, we suggest you do one or more of the following:

\n+

\n+

Make it your own

\n+

Plone has a lot of different settings that can be used to make it do what you want it to. Some examples:

\n+

Tell us how you use it

\n+

Are you doing something interesting with Plone? Big site deployments, interesting use cases? Do you have a company that delivers Plone-based solutions?

\n+

Find out more about Plone

\n+

\n+

Plone is a powerful content management system built on a rock-solid application stack written using the Python programming language. More about these technologies:

\n+

\n+

Support the Plone Foundation

\n+

Plone is made possible only through the efforts of thousands of dedicated individuals and hundreds of companies. The Plone Foundation:

\n+
    \n+
  • \xe2\x80\xa6protects and promotes Plone.
  • \n+
  • \xe2\x80\xa6is a registered 501(c)(3) charitable organization.
  • \n+
  • \xe2\x80\xa6donations are tax-deductible.
  • \n+
\n+

Thanks for using our product; we hope you like it!

\n+

\xe2\x80\x94The Plone Team

\n+ """.format(uid=self.UID)\n+ import time\n+ startTime = time.time()\n+ res = self.parser(text)\n+ executionTime = (time.time() - startTime)\n+ print("\\n\\nimage srcset parsing time: {}\\n".format(executionTime))\n+ self.assertTrue(res)\n+\n+ text_out = """

Welcome!

\n+

If you\'re seeing this instead of the web site you were expecting, the owner of this web site has just installed Plone. Do not contact the Plone Team or the Plone support channels about this.

\n+

\n+

Get started

\n+

Before you start exploring your newly created Plone site, please do the following:

\n+
    \n+
  1. Make sure you are logged in as an admin/manager user. (You should have a Site Setup entry in the user menu)
  2. \n+
\n+

Get comfortable

\n+

After that, we suggest you do one or more of the following:

\n+

\n+

Make it your own

\n+

Plone has a lot of different settings that can be used to make it do what you want it to. Some examples:

\n+

Tell us how you use it

\n+

Are you doing something interesting with Plone? Big site deployments, interesting use cases? Do you have a company that delivers Plone-based solutions?

\n+

Find out more about Plone

\n+

\n+

Plone is a powerful content management system built on a rock-solid application stack written using the Python programming language. More about these technologies:

\n+

\n+

Support the Plone Foundation

\n+

Plone is made possible only through the efforts of thousands of dedicated individuals and hundreds of companies. The Plone Foundation:

\n+
    \n+
  • \xe2\x80\xa6protects and promotes Plone.
  • \n+
  • \xe2\x80\xa6is a registered 501(c)(3) charitable organization.
  • \n+
  • \xe2\x80\xa6donations are tax-deductible.
  • \n+
\n+

Thanks for using our product; we hope you like it!

\n+

\xe2\x80\x94The Plone Team

\n+ """.format(uid=self.UID)\n+ self._assertTransformsTo(text, text_out)\n+\n+ def test_parsing_with_nonexisting_srcset(self):\n+ text = """\n+

\n+ """.format(uid=self.UID)\n+ res = self.parser(text)\n+ self.assertTrue(res)\n+ text_out = """\n+

\n+ """.format(uid=self.UID)\n+ # verify that tag was not converted:\n+ self.assertTrue("data-srcset" in res)\n\\ No newline at end of file\n' -Repository: Products.CMFPlone +Repository: plone.outputfilters Branch: refs/heads/master -Date: 2022-05-17T12:09:56+02:00 +Date: 2022-04-22T14:41:32+03:00 Author: MrTango (MrTango) -Commit: https://github.com/plone/Products.CMFPlone/commit/2dd5c11f190184efc3a9380aaa07db72be115626 +Commit: https://github.com/plone/plone.outputfilters/commit/0812821f85bfd110a9040aa26b038c7fdf20a822 -tinymce pat settings from image_scales to image_srcsets +copy all attributes except src/srcset from images in srcset filter Files changed: -M Products/CMFPlone/patterns/settings.py +M plone/outputfilters/filters/image_srcset.py -b'diff --git a/Products/CMFPlone/patterns/settings.py b/Products/CMFPlone/patterns/settings.py\nindex cb7a236ead..3020d4694b 100644\n--- a/Products/CMFPlone/patterns/settings.py\n+++ b/Products/CMFPlone/patterns/settings.py\n@@ -75,6 +75,7 @@ def mark_special_links(self):\n def image_srcsets(self):\n registry = getUtility(IRegistry)\n settings = registry.forInterface(IImagingSchema, prefix="plone", check=False)\n+<<<<<<< HEAD\n editor_srcsets = {}\n for k, srcset in settings.image_srcsets.items():\n hide_in_editor = srcset.get("hideInEditor")\n@@ -88,6 +89,9 @@ def image_captioning(self):\n registry = getUtility(IRegistry)\n settings = registry.forInterface(IImagingSchema, prefix="plone", check=False)\n return settings.image_captioning\n+=======\n+ return settings.image_srcsets\n+>>>>>>> 6751589c8 (tinymce pat settings from image_scales to image_srcsets)\n \n def tinymce(self):\n """\n@@ -141,7 +145,10 @@ def tinymce(self):\n "base_url": self.context.absolute_url(),\n "imageTypes": image_types,\n "imageSrcsets": self.image_srcsets,\n+<<<<<<< HEAD\n "imageCaptioningEnabled": self.image_captioning,\n+=======\n+>>>>>>> 6751589c8 (tinymce pat settings from image_scales to image_srcsets)\n "linkAttribute": "UID",\n # This is for loading the languages on tinymce\n "loadingBaseUrl": "{}/++plone++static/components/tinymce-builded/"\n' +b'diff --git a/plone/outputfilters/filters/image_srcset.py b/plone/outputfilters/filters/image_srcset.py\nindex ba289c9..d60a61b 100644\n--- a/plone/outputfilters/filters/image_srcset.py\n+++ b/plone/outputfilters/filters/image_srcset.py\n@@ -85,9 +85,6 @@ def convert_to_srcset(self, srcset_name, elem, soup):\n for i, source in enumerate(sourceset):\n scale = source["scale"]\n media = source.get("media")\n- title = elem.attrs.get("title")\n- alt = elem.attrs.get("alt")\n- klass = elem.attrs.get("class")\n if i == len(sourceset) - 1:\n source_tag = soup.new_tag(\n "img", src=self.update_src_scale(src=src, scale=scale)\n@@ -97,15 +94,13 @@ def convert_to_srcset(self, srcset_name, elem, soup):\n source_tag = soup.new_tag(\n "source", srcset=self.update_src_scale(src=src, scale=scale)\n )\n+ for k, attr in elem.attrs.items():\n+ if k in ["src", "srcset"]:\n+ continue\n+ source_tag.attrs[k] = attr\n source_tag["loading"] = "lazy"\n if media:\n source_tag["media"] = media\n- if title:\n- source_tag["title"] = title\n- if alt:\n- source_tag["alt"] = alt\n- if klass:\n- source_tag["class"] = klass\n picture_tag.append(source_tag)\n return picture_tag\n \n' -Repository: Products.CMFPlone +Repository: plone.outputfilters Branch: refs/heads/master -Date: 2022-05-17T12:13:06+02:00 +Date: 2022-04-22T14:44:04+03:00 Author: MrTango (MrTango) -Commit: https://github.com/plone/Products.CMFPlone/commit/49949b881eceea1f5fd0944cdf230fc5204c9997 +Commit: https://github.com/plone/plone.outputfilters/commit/a5c1abc5198b580a0b2f322fb49f28f1b19f842e -filter out srcsets with hideInEditor set to true +set width/height on img and inside srcset Files changed: -M Products/CMFPlone/patterns/settings.py +M plone/outputfilters/filters/resolveuid_and_caption.py -b'diff --git a/Products/CMFPlone/patterns/settings.py b/Products/CMFPlone/patterns/settings.py\nindex 3020d4694b..cb7a236ead 100644\n--- a/Products/CMFPlone/patterns/settings.py\n+++ b/Products/CMFPlone/patterns/settings.py\n@@ -75,7 +75,6 @@ def mark_special_links(self):\n def image_srcsets(self):\n registry = getUtility(IRegistry)\n settings = registry.forInterface(IImagingSchema, prefix="plone", check=False)\n-<<<<<<< HEAD\n editor_srcsets = {}\n for k, srcset in settings.image_srcsets.items():\n hide_in_editor = srcset.get("hideInEditor")\n@@ -89,9 +88,6 @@ def image_captioning(self):\n registry = getUtility(IRegistry)\n settings = registry.forInterface(IImagingSchema, prefix="plone", check=False)\n return settings.image_captioning\n-=======\n- return settings.image_srcsets\n->>>>>>> 6751589c8 (tinymce pat settings from image_scales to image_srcsets)\n \n def tinymce(self):\n """\n@@ -145,10 +141,7 @@ def tinymce(self):\n "base_url": self.context.absolute_url(),\n "imageTypes": image_types,\n "imageSrcsets": self.image_srcsets,\n-<<<<<<< HEAD\n "imageCaptioningEnabled": self.image_captioning,\n-=======\n->>>>>>> 6751589c8 (tinymce pat settings from image_scales to image_srcsets)\n "linkAttribute": "UID",\n # This is for loading the languages on tinymce\n "loadingBaseUrl": "{}/++plone++static/components/tinymce-builded/"\n' +b'diff --git a/plone/outputfilters/filters/resolveuid_and_caption.py b/plone/outputfilters/filters/resolveuid_and_caption.py\nindex 4773bb1..a00085d 100644\n--- a/plone/outputfilters/filters/resolveuid_and_caption.py\n+++ b/plone/outputfilters/filters/resolveuid_and_caption.py\n@@ -171,7 +171,9 @@ def __call__(self, data):\n srcs = [src.strip().split() for src in srcset.strip().split(\',\') if src.strip()]\n for idx, elm in enumerate(srcs):\n image, fullimage, src, description = self.resolve_image(elm[0])\n- srcs[idx][0] = src\n+ # attributes["width"] = image.width\n+ # attributes["height"] = image.height\n+ srcs[idx][0] = "{0} {1}w".format(src, image.width)\n attributes[\'srcset\'] = \',\'.join(\' \'.join(src) for src in srcs)\n for elem in soup.find_all([\'source\', \'iframe\', \'audio\', \'video\']):\n # parent of SOURCE is video or audio here.\n@@ -188,6 +190,9 @@ def __call__(self, data):\n src = attributes.get(\'src\', \'\')\n image, fullimage, src, description = self.resolve_image(src)\n attributes["src"] = src\n+ attributes["width"] = image.width\n+ attributes["height"] = image.height\n+\n \n if fullimage is not None:\n # Check to see if the alt / title tags need setting\n@@ -198,7 +203,11 @@ def __call__(self, data):\n if \'title\' not in attributes:\n attributes[\'title\'] = title\n \n+ # captioning\n caption = description\n+ caption_manual_override = attributes.get("data-captiontext", "")\n+ if caption_manual_override:\n+ caption = caption_manual_override\n # Check if the image needs to be captioned\n if (\n self.captioned_images and\n@@ -245,7 +254,6 @@ def resolve_image(self, src):\n if urlsplit(src)[0]:\n # We have a scheme\n return None, None, src, description\n-\n base = self.context\n subpath = src\n appendix = \'\'\n@@ -328,7 +336,7 @@ def handle_captioned_image(self, attributes, image, fullimage,\n elem, caption):\n """Handle captioned image.\n \n- The img element is replaced by figure\n+ The img/picture element is replaced by figure\n as created by the template ../browser/captioned_image.pt\n """\n klass = \' \'.join(attributes[\'class\'])\n' -Repository: Products.CMFPlone +Repository: plone.outputfilters Branch: refs/heads/master -Date: 2022-06-01T18:17:21+03:00 +Date: 2022-05-05T13:57:19+03:00 Author: MrTango (MrTango) -Commit: https://github.com/plone/Products.CMFPlone/commit/6cb13653c2943b152c9241a7a2b1637ad72480d4 +Commit: https://github.com/plone/plone.outputfilters/commit/8bec6fd6f2070b70a3d2713fef8eec0679aa911e -Merge remote-tracking branch 'remotes/origin/mrtango-image-handling-sourcesets-settings' into mrtango-image-handling-sourcesets-settings +refactor image srcset filter Files changed: -A news/1178.feature -A news/3533.bugfix -M Products/CMFPlone/controlpanel/browser/redirects.py -M Products/CMFPlone/resources/utils.py +M plone/outputfilters/filters/image_srcset.py +M plone/outputfilters/filters/resolveuid_and_caption.py -b'diff --git a/Products/CMFPlone/controlpanel/browser/redirects.py b/Products/CMFPlone/controlpanel/browser/redirects.py\nindex c84c710305..4e84068755 100644\n--- a/Products/CMFPlone/controlpanel/browser/redirects.py\n+++ b/Products/CMFPlone/controlpanel/browser/redirects.py\n@@ -271,7 +271,7 @@ def redirects(self):\n created=self.request.form.get(\'datetime\', \'\'),\n manual=self.request.form.get(\'manual\', \'\'),\n ),\n- 15,\n+ int(self.request.form.get(\'b_size\', \'15\')),\n int(self.request.form.get(\'b_start\', \'0\')),\n orphan=1,\n )\ndiff --git a/Products/CMFPlone/resources/utils.py b/Products/CMFPlone/resources/utils.py\nindex 8a856ae004..3409e53549 100644\n--- a/Products/CMFPlone/resources/utils.py\n+++ b/Products/CMFPlone/resources/utils.py\n@@ -52,7 +52,7 @@ def get_resource(context, path):\n return\n try:\n resource = context.unrestrictedTraverse(path)\n- except (NotFound, AttributeError):\n+ except (NotFound, AttributeError, KeyError):\n logger.warning(\n f"Could not find resource {path}. You may have to create it first."\n ) # noqa\ndiff --git a/news/1178.feature b/news/1178.feature\nnew file mode 100644\nindex 0000000000..af995d9515\n--- /dev/null\n+++ b/news/1178.feature\n@@ -0,0 +1,2 @@\n+Added customisable batch_size for redirects controlpanel\n+[iulianpetchesi]\ndiff --git a/news/3533.bugfix b/news/3533.bugfix\nnew file mode 100644\nindex 0000000000..635ea51c56\n--- /dev/null\n+++ b/news/3533.bugfix\n@@ -0,0 +1,2 @@\n+Fix rendering viewlet.resourceregistries.js when there are missing resources.\n+[petschki]\n' +b'diff --git a/plone/outputfilters/filters/image_srcset.py b/plone/outputfilters/filters/image_srcset.py\nindex d60a61b..f014faa 100644\n--- a/plone/outputfilters/filters/image_srcset.py\n+++ b/plone/outputfilters/filters/image_srcset.py\n@@ -50,6 +50,12 @@ def __init__(self, context=None, request=None):\n self.context = context\n self.request = request\n \n+ @property\n+ def image_scales(self):\n+ registry = getUtility(IRegistry)\n+ settings = registry.forInterface(IImagingSchema, prefix="plone", check=False)\n+ return settings.allowed_sizes\n+\n @property\n def image_srcsets(self):\n registry = getUtility(IRegistry)\n@@ -77,6 +83,10 @@ def convert_to_srcset(self, srcset_name, elem, soup):\n )\n )\n return elem\n+ images_scales = self.image_scales\n+ excluded_scales = srcset_config.get("excludedScales")\n+ if excluded_scales:\n+ images_scales = [scale for scale in self.image_scales if not scale in excluded_scales]\n sourceset = srcset_config.get("sourceset")\n if not sourceset:\n return elem\n@@ -85,9 +95,13 @@ def convert_to_srcset(self, srcset_name, elem, soup):\n for i, source in enumerate(sourceset):\n scale = source["scale"]\n media = source.get("media")\n+ additional_scales = source.get("additionalScales", None)\n+ if additional_scales is None:\n+ additional_scales = [s for s in images_scales if s != scale]\n if i == len(sourceset) - 1:\n source_tag = soup.new_tag(\n- "img", src=self.update_src_scale(src=src, scale=scale)\n+ "img", src=self.update_src_scale(src=src, scale=scale),\n+\n )\n else:\n # TODO guess type:\ndiff --git a/plone/outputfilters/filters/resolveuid_and_caption.py b/plone/outputfilters/filters/resolveuid_and_caption.py\nindex a00085d..dd9b933 100644\n--- a/plone/outputfilters/filters/resolveuid_and_caption.py\n+++ b/plone/outputfilters/filters/resolveuid_and_caption.py\n@@ -171,9 +171,7 @@ def __call__(self, data):\n srcs = [src.strip().split() for src in srcset.strip().split(\',\') if src.strip()]\n for idx, elm in enumerate(srcs):\n image, fullimage, src, description = self.resolve_image(elm[0])\n- # attributes["width"] = image.width\n- # attributes["height"] = image.height\n- srcs[idx][0] = "{0} {1}w".format(src, image.width)\n+ srcs[idx][0] = src\n attributes[\'srcset\'] = \',\'.join(\' \'.join(src) for src in srcs)\n for elem in soup.find_all([\'source\', \'iframe\', \'audio\', \'video\']):\n # parent of SOURCE is video or audio here.\n@@ -190,8 +188,6 @@ def __call__(self, data):\n src = attributes.get(\'src\', \'\')\n image, fullimage, src, description = self.resolve_image(src)\n attributes["src"] = src\n- attributes["width"] = image.width\n- attributes["height"] = image.height\n \n \n if fullimage is not None:\n' -Repository: Products.CMFPlone +Repository: plone.outputfilters Branch: refs/heads/master -Date: 2022-06-07T17:27:08+03:00 +Date: 2022-05-08T21:00:42+03:00 Author: MrTango (MrTango) -Commit: https://github.com/plone/Products.CMFPlone/commit/f16f386703ea8467d165fdb570e83d3dc13e2a41 +Commit: https://github.com/plone/plone.outputfilters/commit/785b0fe4db7bbfc413f7375a2e347a461eda3440 -rename image_srcsets to picture_variants +set w parameter in srcset definitions Files changed: -M Products/CMFPlone/patterns/settings.py +M plone/outputfilters/filters/image_srcset.py -b'diff --git a/Products/CMFPlone/patterns/settings.py b/Products/CMFPlone/patterns/settings.py\nindex cb7a236ead..7f1fb5979e 100644\n--- a/Products/CMFPlone/patterns/settings.py\n+++ b/Products/CMFPlone/patterns/settings.py\n@@ -72,16 +72,16 @@ def mark_special_links(self):\n return result\n \n @property\n- def image_srcsets(self):\n+ def picture_variants(self):\n registry = getUtility(IRegistry)\n settings = registry.forInterface(IImagingSchema, prefix="plone", check=False)\n- editor_srcsets = {}\n- for k, srcset in settings.image_srcsets.items():\n- hide_in_editor = srcset.get("hideInEditor")\n+ editor_picture_variants = {}\n+ for k, picture_variant in settings.picture_variants.items():\n+ hide_in_editor = picture_variant.get("hideInEditor")\n if hide_in_editor:\n continue\n- editor_srcsets[k] = srcset\n- return editor_srcsets\n+ editor_picture_variants[k] = picture_variant\n+ return editor_picture_variants\n \n @property\n def image_captioning(self):\n@@ -140,7 +140,7 @@ def tinymce(self):\n configuration = {\n "base_url": self.context.absolute_url(),\n "imageTypes": image_types,\n- "imageSrcsets": self.image_srcsets,\n+ "pictureVariants": self.picture_variants,\n "imageCaptioningEnabled": self.image_captioning,\n "linkAttribute": "UID",\n # This is for loading the languages on tinymce\n' +b'diff --git a/plone/outputfilters/filters/image_srcset.py b/plone/outputfilters/filters/image_srcset.py\nindex f014faa..1819641 100644\n--- a/plone/outputfilters/filters/image_srcset.py\n+++ b/plone/outputfilters/filters/image_srcset.py\n@@ -51,7 +51,7 @@ def __init__(self, context=None, request=None):\n self.request = request\n \n @property\n- def image_scales(self):\n+ def allowed_scales(self):\n registry = getUtility(IRegistry)\n settings = registry.forInterface(IImagingSchema, prefix="plone", check=False)\n return settings.allowed_sizes\n@@ -62,6 +62,24 @@ def image_srcsets(self):\n settings = registry.forInterface(IImagingSchema, prefix="plone", check=False)\n return settings.image_srcsets\n \n+ def get_scale_name(self, scale_line):\n+ parts = scale_line.split(" ")\n+ return parts and parts[0] or ""\n+\n+ def get_scale_width(self, scale):\n+ """ get width from allowed_scales line\n+ large 800:65536\n+ """\n+ for s in self.allowed_scales:\n+ parts = s.split(" ")\n+ if not parts:\n+ continue\n+ if parts[0] == scale:\n+ dimentions = parts[1].split(":")\n+ if not dimentions:\n+ continue\n+ return dimentions[0]\n+\n def __call__(self, data):\n data = re.sub(r"<([^<>\\s]+?)\\s*/>", self._shorttag_replace, data)\n soup = BeautifulSoup(safe_nativestring(data), "html.parser")\n@@ -83,39 +101,43 @@ def convert_to_srcset(self, srcset_name, elem, soup):\n )\n )\n return elem\n- images_scales = self.image_scales\n- excluded_scales = srcset_config.get("excludedScales")\n- if excluded_scales:\n- images_scales = [scale for scale in self.image_scales if not scale in excluded_scales]\n+ allowed_scales = self.allowed_scales\n sourceset = srcset_config.get("sourceset")\n if not sourceset:\n return elem\n src = elem.attrs.get("src")\n picture_tag = soup.new_tag("picture")\n for i, source in enumerate(sourceset):\n- scale = source["scale"]\n+ target_scale = source["scale"]\n media = source.get("media")\n+\n additional_scales = source.get("additionalScales", None)\n if additional_scales is None:\n- additional_scales = [s for s in images_scales if s != scale]\n- if i == len(sourceset) - 1:\n- source_tag = soup.new_tag(\n- "img", src=self.update_src_scale(src=src, scale=scale),\n-\n- )\n- else:\n- # TODO guess type:\n- source_tag = soup.new_tag(\n- "source", srcset=self.update_src_scale(src=src, scale=scale)\n- )\n- for k, attr in elem.attrs.items():\n- if k in ["src", "srcset"]:\n- continue\n- source_tag.attrs[k] = attr\n- source_tag["loading"] = "lazy"\n+ additional_scales = [self.get_scale_name(s) for s in allowed_scales if s != target_scale]\n+ source_scales = [target_scale] + additional_scales\n+ source_srcset = []\n+ for scale in source_scales:\n+ scale_url = self.update_src_scale(src=src, scale=scale)\n+ scale_width = self.get_scale_width(scale)\n+ source_srcset.append("{0} {1}w".format(scale_url, scale_width))\n+ source_tag = soup.new_tag(\n+ "source", srcset=",\\n".join(source_srcset)\n+ )\n if media:\n source_tag["media"] = media\n picture_tag.append(source_tag)\n+ if i == len(sourceset) - 1:\n+ scale_width = self.get_scale_width(target_scale)\n+ img_tag = soup.new_tag(\n+ "img", src=self.update_src_scale(src=src, scale=target_scale),\n+ )\n+ for k, attr in elem.attrs.items():\n+ if k in ["src", "srcset"]:\n+ continue\n+ img_tag.attrs[k] = attr\n+ img_tag["width"] = scale_width\n+ img_tag["loading"] = "lazy"\n+ picture_tag.append(img_tag)\n return picture_tag\n \n def update_src_scale(self, src, scale):\n' -Repository: Products.CMFPlone +Repository: plone.outputfilters Branch: refs/heads/master -Date: 2022-06-08T23:42:00+02:00 +Date: 2022-05-08T21:15:25+03:00 +Author: MrTango (MrTango) +Commit: https://github.com/plone/plone.outputfilters/commit/4e1b6cd7e40c94121ced22339b66c0c0f7ed1ed9 + +Add img width/height in resolveuid_and_caption filter if not present + +Files changed: +M plone/outputfilters/filters/image_srcset.py +M plone/outputfilters/filters/resolveuid_and_caption.py + +b'diff --git a/plone/outputfilters/filters/image_srcset.py b/plone/outputfilters/filters/image_srcset.py\nindex 1819641..720334d 100644\n--- a/plone/outputfilters/filters/image_srcset.py\n+++ b/plone/outputfilters/filters/image_srcset.py\n@@ -127,7 +127,6 @@ def convert_to_srcset(self, srcset_name, elem, soup):\n source_tag["media"] = media\n picture_tag.append(source_tag)\n if i == len(sourceset) - 1:\n- scale_width = self.get_scale_width(target_scale)\n img_tag = soup.new_tag(\n "img", src=self.update_src_scale(src=src, scale=target_scale),\n )\n@@ -135,7 +134,6 @@ def convert_to_srcset(self, srcset_name, elem, soup):\n if k in ["src", "srcset"]:\n continue\n img_tag.attrs[k] = attr\n- img_tag["width"] = scale_width\n img_tag["loading"] = "lazy"\n picture_tag.append(img_tag)\n return picture_tag\ndiff --git a/plone/outputfilters/filters/resolveuid_and_caption.py b/plone/outputfilters/filters/resolveuid_and_caption.py\nindex dd9b933..d0a0351 100644\n--- a/plone/outputfilters/filters/resolveuid_and_caption.py\n+++ b/plone/outputfilters/filters/resolveuid_and_caption.py\n@@ -188,6 +188,10 @@ def __call__(self, data):\n src = attributes.get(\'src\', \'\')\n image, fullimage, src, description = self.resolve_image(src)\n attributes["src"] = src\n+ if not attributes.get("width"):\n+ attributes["width"] = image.width\n+ if not attributes.get("height"):\n+ attributes["height"] = image.height\n \n \n if fullimage is not None:\n' + +Repository: plone.outputfilters + + +Branch: refs/heads/master +Date: 2022-05-16T18:02:58+02:00 +Author: MrTango (MrTango) +Commit: https://github.com/plone/plone.outputfilters/commit/0ba15c76388499cbe89c5e79321df632ed6cc5d0 + +use new url method in @@images view to postpone image scale creating until browser reuqests it + +Files changed: +M plone/outputfilters/filters/resolveuid_and_caption.py + +b'diff --git a/plone/outputfilters/filters/resolveuid_and_caption.py b/plone/outputfilters/filters/resolveuid_and_caption.py\nindex d0a0351..2e0d80c 100644\n--- a/plone/outputfilters/filters/resolveuid_and_caption.py\n+++ b/plone/outputfilters/filters/resolveuid_and_caption.py\n@@ -170,7 +170,8 @@ def __call__(self, data):\n # [(src1, 480w), (src2, 360w)]\n srcs = [src.strip().split() for src in srcset.strip().split(\',\') if src.strip()]\n for idx, elm in enumerate(srcs):\n- image, fullimage, src, description = self.resolve_image(elm[0])\n+ image_url = elm[0]\n+ src = self.resolve_scale_data(image_url)\n srcs[idx][0] = src\n attributes[\'srcset\'] = \',\'.join(\' \'.join(src) for src in srcs)\n for elem in soup.find_all([\'source\', \'iframe\', \'audio\', \'video\']):\n@@ -188,12 +189,11 @@ def __call__(self, data):\n src = attributes.get(\'src\', \'\')\n image, fullimage, src, description = self.resolve_image(src)\n attributes["src"] = src\n- if not attributes.get("width"):\n- attributes["width"] = image.width\n- if not attributes.get("height"):\n- attributes["height"] = image.height\n-\n-\n+ # we could get the width/height (aspect ratio) without the scale\n+ # from the image field: width, height = fullimage.get("image").getImageSize()\n+ # XXX: refacture resolve_image to not create scales\n+ attributes["width"] = image.width\n+ attributes["height"] = image.height\n if fullimage is not None:\n # Check to see if the alt / title tags need setting\n title = safe_unicode(aq_acquire(fullimage, \'Title\')())\n@@ -229,6 +229,16 @@ def lookup_uid(self, uid):\n uid = uids[0]\n return uuidToObject(uid)\n \n+ def resolve_scale_data(self, url):\n+ """ return scale url, width and height\n+ """\n+ url_parts = url.split("/")\n+ field_name = url_parts[-2]\n+ scale_name = url_parts[-1]\n+ obj, subpath, appendix = self.resolve_link(url)\n+ scale_view = obj.unrestrictedTraverse(\'@@images\', None)\n+ return scale_view.url(field=field_name, scale=scale_name)\n+\n def resolve_link(self, href):\n obj = None\n subpath = href\n@@ -270,9 +280,9 @@ def traversal_stack(base, path):\n try:\n if hasattr(aq_base(obj), \'scale\'):\n if components:\n- child = obj.scale(child_id, components.pop())\n+ child = obj.url(child_id, components.pop())\n else:\n- child = obj.scale(child_id)\n+ child = obj.url(child_id)\n else:\n # Do not use restrictedTraverse here; the path to the\n # image may lead over containers that lack the View\n@@ -323,7 +333,6 @@ def traverse_path(base, path):\n \n if image is None:\n return None, None, src, description\n-\n try:\n url = image.absolute_url()\n except AttributeError:\n' + +Repository: plone.outputfilters + + +Branch: refs/heads/master +Date: 2022-05-16T22:15:05+02:00 +Author: MrTango (MrTango) +Commit: https://github.com/plone/plone.outputfilters/commit/7528e725db1d662894ea86f41fda2fe0f54cd4c4 + +use new pre scale parameter + +Files changed: +M plone/outputfilters/filters/resolveuid_and_caption.py + +b"diff --git a/plone/outputfilters/filters/resolveuid_and_caption.py b/plone/outputfilters/filters/resolveuid_and_caption.py\nindex 2e0d80c..861de3e 100644\n--- a/plone/outputfilters/filters/resolveuid_and_caption.py\n+++ b/plone/outputfilters/filters/resolveuid_and_caption.py\n@@ -171,7 +171,7 @@ def __call__(self, data):\n srcs = [src.strip().split() for src in srcset.strip().split(',') if src.strip()]\n for idx, elm in enumerate(srcs):\n image_url = elm[0]\n- src = self.resolve_scale_data(image_url)\n+ image, fullimage, src, description = self.resolve_image(image_url)\n srcs[idx][0] = src\n attributes['srcset'] = ','.join(' '.join(src) for src in srcs)\n for elem in soup.find_all(['source', 'iframe', 'audio', 'video']):\n@@ -237,7 +237,7 @@ def resolve_scale_data(self, url):\n scale_name = url_parts[-1]\n obj, subpath, appendix = self.resolve_link(url)\n scale_view = obj.unrestrictedTraverse('@@images', None)\n- return scale_view.url(field=field_name, scale=scale_name)\n+ return scale_view.scale(field_name, scale_name, pre=True)\n \n def resolve_link(self, href):\n obj = None\n@@ -280,9 +280,9 @@ def traversal_stack(base, path):\n try:\n if hasattr(aq_base(obj), 'scale'):\n if components:\n- child = obj.url(child_id, components.pop())\n+ child = obj.scale(child_id, components.pop(), pre=True)\n else:\n- child = obj.url(child_id)\n+ child = obj.scale(child_id, pre=True)\n else:\n # Do not use restrictedTraverse here; the path to the\n # image may lead over containers that lack the View\n" + +Repository: plone.outputfilters + + +Branch: refs/heads/master +Date: 2022-05-18T17:43:15+02:00 +Author: MrTango (MrTango) +Commit: https://github.com/plone/plone.outputfilters/commit/8dbc3366397cbef1ecd92cd4cb60de971903f7cd + +Fix captioning of picture tags + +Files changed: +M plone/outputfilters/browser/captioned_image.pt +M plone/outputfilters/filters/configure.zcml +M plone/outputfilters/filters/image_srcset.py +M plone/outputfilters/filters/resolveuid_and_caption.py + +b'diff --git a/plone/outputfilters/browser/captioned_image.pt b/plone/outputfilters/browser/captioned_image.pt\nindex 8f4a9a8..637ba2a 100644\n--- a/plone/outputfilters/browser/captioned_image.pt\n+++ b/plone/outputfilters/browser/captioned_image.pt\n@@ -1,6 +1,4 @@\n
\n- [image goes here]\n+ \n
\n
\ndiff --git a/plone/outputfilters/filters/configure.zcml b/plone/outputfilters/filters/configure.zcml\nindex 0899ce7..c351214 100644\n--- a/plone/outputfilters/filters/configure.zcml\n+++ b/plone/outputfilters/filters/configure.zcml\n@@ -5,16 +5,16 @@\n \n \n \n \n \n \ndiff --git a/plone/outputfilters/filters/image_srcset.py b/plone/outputfilters/filters/image_srcset.py\nindex 720334d..62d4348 100644\n--- a/plone/outputfilters/filters/image_srcset.py\n+++ b/plone/outputfilters/filters/image_srcset.py\n@@ -107,6 +107,9 @@ def convert_to_srcset(self, srcset_name, elem, soup):\n return elem\n src = elem.attrs.get("src")\n picture_tag = soup.new_tag("picture")\n+ css_classes = elem.attrs.get("class")\n+ if "captioned" in css_classes:\n+ picture_tag["class"] = "captioned"\n for i, source in enumerate(sourceset):\n target_scale = source["scale"]\n media = source.get("media")\ndiff --git a/plone/outputfilters/filters/resolveuid_and_caption.py b/plone/outputfilters/filters/resolveuid_and_caption.py\nindex 861de3e..b0bc572 100644\n--- a/plone/outputfilters/filters/resolveuid_and_caption.py\n+++ b/plone/outputfilters/filters/resolveuid_and_caption.py\n@@ -203,20 +203,36 @@ def __call__(self, data):\n if \'title\' not in attributes:\n attributes[\'title\'] = title\n \n- # captioning\n+ for picture_elem in soup.find_all(\'picture\'):\n+ if \'captioned\' not in picture_elem.attrs.get(\'class\', []):\n+ continue\n+ elem = picture_elem.find("img")\n+ attributes = elem.attrs\n+ src = attributes.get(\'src\', \'\')\n+ image, fullimage, src, description = self.resolve_image(src)\n+ attributes["src"] = src\n caption = description\n caption_manual_override = attributes.get("data-captiontext", "")\n if caption_manual_override:\n caption = caption_manual_override\n # Check if the image needs to be captioned\n- if (\n- self.captioned_images and\n- image is not None and\n- caption and\n- \'captioned\' in attributes.get(\'class\', [])\n- ):\n- self.handle_captioned_image(\n- attributes, image, fullimage, elem, caption)\n+ if (self.captioned_images and caption):\n+ options = {}\n+ options["tag"] = picture_elem.prettify()\n+ options["caption"] = newline_to_br(html_quote(caption))\n+ options["class"] = \' \'.join(attributes[\'class\'])\n+ del attributes[\'class\']\n+ picture_elem.append(elem)\n+ captioned = BeautifulSoup(\n+ self.captioned_image_template(**options), \'html.parser\')\n+\n+ # if we are a captioned image within a link, remove and occurrences\n+ # of a tags inside caption template to preserve the outer link\n+ if bool(elem.find_parent(\'a\')) and bool(captioned.find(\'a\')):\n+ captioned.a.unwrap()\n+ del captioned.picture.img["class"]\n+ picture_elem.replace_with(captioned)\n+\n return six.text_type(soup)\n \n def lookup_uid(self, uid):\n@@ -340,58 +356,3 @@ def traverse_path(base, path):\n src = url + appendix\n description = safe_unicode(aq_acquire(fullimage, \'Description\')())\n return image, fullimage, src, description\n-\n- def handle_captioned_image(self, attributes, image, fullimage,\n- elem, caption):\n- """Handle captioned image.\n-\n- The img/picture element is replaced by figure\n- as created by the template ../browser/captioned_image.pt\n- """\n- klass = \' \'.join(attributes[\'class\'])\n- del attributes[\'class\']\n- del attributes[\'src\']\n- if \'width\' in attributes and attributes[\'width\']:\n- attributes[\'width\'] = int(attributes[\'width\'])\n- if \'height\' in attributes and attributes[\'height\']:\n- attributes[\'height\'] = int(attributes[\'height\'])\n- view = fullimage.unrestrictedTraverse(\'@@images\', None)\n- if view is not None:\n- original_width, original_height = view.getImageSize()\n- else:\n- original_width, original_height = fullimage.width, fullimage.height\n- if image is not fullimage:\n- # image is a scale object\n- tag = image.tag\n- width = image.width\n- else:\n- if hasattr(aq_base(image), \'tag\'):\n- tag = image.tag\n- else:\n- tag = view.scale().tag\n- width = original_width\n- options = {\n- \'class\': klass,\n- \'originalwidth\': attributes.get(\'width\', None),\n- \'originalalt\': attributes.get(\'alt\', None),\n- \'url_path\': fullimage.absolute_url_path(),\n- \'caption\': newline_to_br(html_quote(caption)),\n- \'image\': image,\n- \'fullimage\': fullimage,\n- \'tag\': tag(**attributes),\n- \'isfullsize\': image is fullimage or (\n- image.width == original_width and\n- image.height == original_height),\n- \'width\': attributes.get(\'width\', width),\n- }\n-\n- captioned = BeautifulSoup(\n- self.captioned_image_template(**options), \'html.parser\')\n-\n- # if we are a captioned image within a link, remove and occurrences\n- # of a tags inside caption template to preserve the outer link\n- if bool(elem.find_parent(\'a\')) and \\\n- bool(captioned.find(\'a\')):\n- captioned.a.unwrap()\n-\n- elem.replace_with(captioned)\n' + +Repository: plone.outputfilters + + +Branch: refs/heads/master +Date: 2022-05-24T20:44:17+03:00 +Author: MrTango (MrTango) +Commit: https://github.com/plone/plone.outputfilters/commit/17157fd14cd4e5e90339afc16c1e183fa658956e + +do not use title as alt attribute content + +Files changed: +M plone/outputfilters/filters/resolveuid_and_caption.py + +b'diff --git a/plone/outputfilters/filters/resolveuid_and_caption.py b/plone/outputfilters/filters/resolveuid_and_caption.py\nindex b0bc572..7a18028 100644\n--- a/plone/outputfilters/filters/resolveuid_and_caption.py\n+++ b/plone/outputfilters/filters/resolveuid_and_caption.py\n@@ -199,7 +199,7 @@ def __call__(self, data):\n title = safe_unicode(aq_acquire(fullimage, \'Title\')())\n if not attributes.get(\'alt\'):\n # XXX alt attribute contains *alternate* text\n- attributes[\'alt\'] = description or title\n+ attributes[\'alt\'] = description or ""\n if \'title\' not in attributes:\n attributes[\'title\'] = title\n \n' + +Repository: plone.outputfilters + + +Branch: refs/heads/master +Date: 2022-06-01T21:00:06+03:00 +Author: MrTango (MrTango) +Commit: https://github.com/plone/plone.outputfilters/commit/ce96bc9be3975344fbc84f048e406a66873b1108 + +refacture image_srcset method to be reusable in plone.namedfile + +Files changed: +A plone/outputfilters/utils.py +M plone/outputfilters/filters/image_srcset.py + +b'diff --git a/plone/outputfilters/filters/image_srcset.py b/plone/outputfilters/filters/image_srcset.py\nindex 62d4348..c837c07 100644\n--- a/plone/outputfilters/filters/image_srcset.py\n+++ b/plone/outputfilters/filters/image_srcset.py\n@@ -2,32 +2,17 @@\n import re\n \n from bs4 import BeautifulSoup\n-from plone.base.interfaces import IImagingSchema\n from plone.outputfilters.interfaces import IFilter\n-from plone.registry.interfaces import IRegistry\n from Products.CMFPlone.utils import safe_nativestring\n-from zope.component import getUtility\n from zope.interface import implementer\n+from plone.outputfilters.utils import Img2PictureTag\n \n logger = logging.getLogger("plone.outputfilter.image_srcset")\n \n \n @implementer(IFilter)\n class ImageSrcsetFilter(object):\n- """Converts img/figure tags with a data-srcset attribute into srcset definition.\n- \n- \n- \n- \n- \n- \n- \n- \n+ """Converts img tags with a data-srcset attribute into picture tag with srcset definition.\n """\n \n order = 700\n@@ -49,36 +34,8 @@ def __init__(self, context=None, request=None):\n self.current_status = None\n self.context = context\n self.request = request\n+ self.img2picturetag = Img2PictureTag()\n \n- @property\n- def allowed_scales(self):\n- registry = getUtility(IRegistry)\n- settings = registry.forInterface(IImagingSchema, prefix="plone", check=False)\n- return settings.allowed_sizes\n-\n- @property\n- def image_srcsets(self):\n- registry = getUtility(IRegistry)\n- settings = registry.forInterface(IImagingSchema, prefix="plone", check=False)\n- return settings.image_srcsets\n-\n- def get_scale_name(self, scale_line):\n- parts = scale_line.split(" ")\n- return parts and parts[0] or ""\n-\n- def get_scale_width(self, scale):\n- """ get width from allowed_scales line\n- large 800:65536\n- """\n- for s in self.allowed_scales:\n- parts = s.split(" ")\n- if not parts:\n- continue\n- if parts[0] == scale:\n- dimentions = parts[1].split(":")\n- if not dimentions:\n- continue\n- return dimentions[0]\n \n def __call__(self, data):\n data = re.sub(r"<([^<>\\s]+?)\\s*/>", self._shorttag_replace, data)\n@@ -88,59 +45,16 @@ def __call__(self, data):\n srcset_name = elem.attrs.get("data-srcset", "")\n if not srcset_name:\n continue\n- elem.replace_with(self.convert_to_srcset(srcset_name, elem, soup))\n- return str(soup)\n-\n- def convert_to_srcset(self, srcset_name, elem, soup):\n- """Converts the element to a srcset definition"""\n- srcset_config = self.image_srcsets.get(srcset_name)\n- if not srcset_config:\n- logger.warn(\n- "Could not find the given srcset_name {0}, leave tag untouched!".format(\n- srcset_name\n+ srcset_config = self.img2picturetag.image_srcsets.get(srcset_name)\n+ if not srcset_config:\n+ logger.warn(\n+ "Could not find the given srcset_name {0}, leave tag untouched!".format(\n+ srcset_name\n+ )\n )\n- )\n- return elem\n- allowed_scales = self.allowed_scales\n- sourceset = srcset_config.get("sourceset")\n- if not sourceset:\n- return elem\n- src = elem.attrs.get("src")\n- picture_tag = soup.new_tag("picture")\n- css_classes = elem.attrs.get("class")\n- if "captioned" in css_classes:\n- picture_tag["class"] = "captioned"\n- for i, source in enumerate(sourceset):\n- target_scale = source["scale"]\n- media = source.get("media")\n-\n- additional_scales = source.get("additionalScales", None)\n- if additional_scales is None:\n- additional_scales = [self.get_scale_name(s) for s in allowed_scales if s != target_scale]\n- source_scales = [target_scale] + additional_scales\n- source_srcset = []\n- for scale in source_scales:\n- scale_url = self.update_src_scale(src=src, scale=scale)\n- scale_width = self.get_scale_width(scale)\n- source_srcset.append("{0} {1}w".format(scale_url, scale_width))\n- source_tag = soup.new_tag(\n- "source", srcset=",\\n".join(source_srcset)\n- )\n- if media:\n- source_tag["media"] = media\n- picture_tag.append(source_tag)\n- if i == len(sourceset) - 1:\n- img_tag = soup.new_tag(\n- "img", src=self.update_src_scale(src=src, scale=target_scale),\n- )\n- for k, attr in elem.attrs.items():\n- if k in ["src", "srcset"]:\n- continue\n- img_tag.attrs[k] = attr\n- img_tag["loading"] = "lazy"\n- picture_tag.append(img_tag)\n- return picture_tag\n-\n- def update_src_scale(self, src, scale):\n- parts = src.split("/")\n- return "/".join(parts[:-1]) + "/{}".format(scale)\n+ continue\n+ sourceset = srcset_config.get("sourceset")\n+ if not sourceset:\n+ continue\n+ elem.replace_with(self.img2picturetag.create_picture_tag(sourceset, elem.attrs))\n+ return str(soup)\ndiff --git a/plone/outputfilters/utils.py b/plone/outputfilters/utils.py\nnew file mode 100644\nindex 0000000..7184721\n--- /dev/null\n+++ b/plone/outputfilters/utils.py\n@@ -0,0 +1,88 @@\n+import logging\n+\n+from plone.base.interfaces import IImagingSchema\n+from plone.registry.interfaces import IRegistry\n+from zope.component import getUtility\n+from bs4 import BeautifulSoup\n+\n+logger = logging.getLogger("plone.outputfilter.image_srcset")\n+\n+\n+class Img2PictureTag(object):\n+ @property\n+ def allowed_scales(self):\n+ registry = getUtility(IRegistry)\n+ settings = registry.forInterface(IImagingSchema, prefix="plone", check=False)\n+ return settings.allowed_sizes\n+\n+ @property\n+ def image_srcsets(self):\n+ registry = getUtility(IRegistry)\n+ settings = registry.forInterface(IImagingSchema, prefix="plone", check=False)\n+ return settings.image_srcsets\n+\n+ def get_scale_name(self, scale_line):\n+ parts = scale_line.split(" ")\n+ return parts and parts[0] or ""\n+\n+ def get_scale_width(self, scale):\n+ """get width from allowed_scales line\n+ large 800:65536\n+ """\n+ for s in self.allowed_scales:\n+ parts = s.split(" ")\n+ if not parts:\n+ continue\n+ if parts[0] == scale:\n+ dimentions = parts[1].split(":")\n+ if not dimentions:\n+ continue\n+ return dimentions[0]\n+\n+ def create_picture_tag(self, sourceset, attributes):\n+ """Converts the element to a srcset definition"""\n+ soup = BeautifulSoup("", "html.parser")\n+ allowed_scales = self.allowed_scales\n+ src = attributes.get("src")\n+ picture_tag = soup.new_tag("picture")\n+ css_classes = attributes.get("class")\n+ if "captioned" in css_classes:\n+ picture_tag["class"] = "captioned"\n+ for i, source in enumerate(sourceset):\n+ target_scale = source["scale"]\n+ media = source.get("media")\n+\n+ additional_scales = source.get("additionalScales", None)\n+ if additional_scales is None:\n+ additional_scales = [\n+ self.get_scale_name(s) for s in allowed_scales if s != target_scale\n+ ]\n+ source_scales = [target_scale] + additional_scales\n+ source_srcset = []\n+ for scale in source_scales:\n+ scale_url = self.update_src_scale(src=src, scale=scale)\n+ scale_width = self.get_scale_width(scale)\n+ source_srcset.append("{0} {1}w".format(scale_url, scale_width))\n+ source_tag = soup.new_tag("source", srcset=",\\n".join(source_srcset))\n+ if media:\n+ source_tag["media"] = media\n+ picture_tag.append(source_tag)\n+ if i == len(sourceset) - 1:\n+ img_tag = soup.new_tag(\n+ "img",\n+ src=self.update_src_scale(src=src, scale=target_scale),\n+ )\n+ for k, attr in attributes.items():\n+ if k in ["src", "srcset"]:\n+ continue\n+ img_tag.attrs[k] = attr\n+ img_tag["loading"] = "lazy"\n+ picture_tag.append(img_tag)\n+ return picture_tag\n+\n+ def update_src_scale(self, src, scale):\n+ parts = src.split("/")\n+ if "." in src:\n+ field_name = parts[-1].split("-")[0]\n+ return "/".join(parts[:-1]) + "/{0}/{1}".format(field_name, scale)\n+ return "/".join(parts[:-1]) + "/{}".format(scale)\n' + +Repository: plone.outputfilters + + +Branch: refs/heads/master +Date: 2022-06-02T10:35:02+03:00 +Author: MrTango (MrTango) +Commit: https://github.com/plone/plone.outputfilters/commit/b2b2f8fc17101d702ca99065bf1fd3256e80a29a + +fix update_src_scale method + +Files changed: +M plone/outputfilters/filters/resolveuid_and_caption.py +M plone/outputfilters/utils.py + +b'diff --git a/plone/outputfilters/filters/resolveuid_and_caption.py b/plone/outputfilters/filters/resolveuid_and_caption.py\nindex 7a18028..3725e1a 100644\n--- a/plone/outputfilters/filters/resolveuid_and_caption.py\n+++ b/plone/outputfilters/filters/resolveuid_and_caption.py\n@@ -192,6 +192,8 @@ def __call__(self, data):\n # we could get the width/height (aspect ratio) without the scale\n # from the image field: width, height = fullimage.get("image").getImageSize()\n # XXX: refacture resolve_image to not create scales\n+ if not image:\n+ return\n attributes["width"] = image.width\n attributes["height"] = image.height\n if fullimage is not None:\ndiff --git a/plone/outputfilters/utils.py b/plone/outputfilters/utils.py\nindex 7184721..22038ad 100644\n--- a/plone/outputfilters/utils.py\n+++ b/plone/outputfilters/utils.py\n@@ -82,7 +82,9 @@ def create_picture_tag(self, sourceset, attributes):\n \n def update_src_scale(self, src, scale):\n parts = src.split("/")\n- if "." in src:\n+ if "." in parts[-1]:\n field_name = parts[-1].split("-")[0]\n- return "/".join(parts[:-1]) + "/{0}/{1}".format(field_name, scale)\n- return "/".join(parts[:-1]) + "/{}".format(scale)\n+ src_scale = "/".join(parts[:-1]) + "/{0}/{1}".format(field_name, scale)\n+ return src_scale\n+ src_scale = "/".join(parts[:-1]) + "/{}".format(scale)\n+ return src_scale\n' + +Repository: plone.outputfilters + + +Branch: refs/heads/master +Date: 2022-06-02T11:31:39+03:00 +Author: MrTango (MrTango) +Commit: https://github.com/plone/plone.outputfilters/commit/8371ce079639ba63f7151b988cbd013894722f07 + +Fix image_srcset tests + +Files changed: +M plone/outputfilters/tests/test_image_srcset.py + +b'diff --git a/plone/outputfilters/tests/test_image_srcset.py b/plone/outputfilters/tests/test_image_srcset.py\nindex 0d56a7a..38a7e7f 100644\n--- a/plone/outputfilters/tests/test_image_srcset.py\n+++ b/plone/outputfilters/tests/test_image_srcset.py\n@@ -119,30 +119,41 @@ def test_parsing_minimal(self):\n \n def test_parsing_long_doc(self):\n text = """

Welcome!

\n-

If you\'re seeing this instead of the web site you were expecting, the owner of this web site has just installed Plone. Do not contact the Plone Team or the Plone support channels about this.

\n-

\n+

If you\'re seeing this instead of the web site you were expecting, the owner of this web site has\n+ just installed Plone. Do not contact the Plone Team or the Plone support channels about this.

\n+

\n

Get started

\n

Before you start exploring your newly created Plone site, please do the following:

\n
    \n-
  1. Make sure you are logged in as an admin/manager user. (You should have a Site Setup entry in the user menu)
  2. \n+
  3. Make sure you are logged in as an admin/manager user. (You should have a Site Setup entry\n+ in the user menu)
  4. \n
\n

Get comfortable

\n

After that, we suggest you do one or more of the following:

\n-

\n+

\n

Make it your own

\n

Plone has a lot of different settings that can be used to make it do what you want it to. Some examples:

\n

Tell us how you use it

\n-

Are you doing something interesting with Plone? Big site deployments, interesting use cases? Do you have a company that delivers Plone-based solutions?

\n+

Are you doing something interesting with Plone? Big site deployments, interesting use cases? Do you have a company\n+ that delivers Plone-based solutions?

\n

Find out more about Plone

\n-

\n-

Plone is a powerful content management system built on a rock-solid application stack written using the Python programming language. More about these technologies:

\n-

\n+

\n+

Plone is a powerful content management system built on a rock-solid application stack written using the Python\n+ programming language. More about these technologies:

\n+

\n

Support the Plone Foundation

\n-

Plone is made possible only through the efforts of thousands of dedicated individuals and hundreds of companies. The Plone Foundation:

\n+

Plone is made possible only through the efforts of thousands of dedicated individuals and hundreds of companies. The\n+ Plone Foundation:

\n
    \n-
  • \xe2\x80\xa6protects and promotes Plone.
  • \n-
  • \xe2\x80\xa6is a registered 501(c)(3) charitable organization.
  • \n-
  • \xe2\x80\xa6donations are tax-deductible.
  • \n+
  • \xe2\x80\xa6protects and promotes Plone.
  • \n+
  • \xe2\x80\xa6is a registered 501(c)(3) charitable organization.
  • \n+
  • \xe2\x80\xa6donations are tax-deductible.
  • \n
\n

Thanks for using our product; we hope you like it!

\n

\xe2\x80\x94The Plone Team

\n@@ -155,35 +166,72 @@ def test_parsing_long_doc(self):\n self.assertTrue(res)\n \n text_out = """

Welcome!

\n-

If you\'re seeing this instead of the web site you were expecting, the owner of this web site has just installed Plone. Do not contact the Plone Team or the Plone support channels about this.

\n-

\n+

If you\'re seeing this instead of the web site you were expecting, the owner of this web site has\n+ just installed Plone. Do not contact the Plone Team or the Plone support channels about this.

\n+

\n+ \n+ \n+ \n+ \n+

\n

Get started

\n

Before you start exploring your newly created Plone site, please do the following:

\n
    \n-
  1. Make sure you are logged in as an admin/manager user. (You should have a Site Setup entry in the user menu)
  2. \n+
  3. Make sure you are logged in as an admin/manager user.(You should have a Site Setup entry\n+ in the user menu)
  4. \n
\n

Get comfortable

\n

After that, we suggest you do one or more of the following:

\n-

\n+

\n+ \n+ \n+ \n+ \n+

\n

Make it your own

\n

Plone has a lot of different settings that can be used to make it do what you want it to. Some examples:

\n

Tell us how you use it

\n-

Are you doing something interesting with Plone? Big site deployments, interesting use cases? Do you have a company that delivers Plone-based solutions?

\n+

Are you doing something interesting with Plone? Big site deployments, interesting use cases? Do you have a company\n+ that delivers Plone-based solutions?

\n

Find out more about Plone

\n-

\n-

Plone is a powerful content management system built on a rock-solid application stack written using the Python programming language. More about these technologies:

\n-

\n+

\n+ \n+ \n+ \n+ \n+

\n+

Plone is a powerful content management system built on a rock-solid application stack written using the Python\n+ programming language. More about these technologies:

\n+

\n+ \n+ \n+ \n+ \n+

\n

Support the Plone Foundation

\n-

Plone is made possible only through the efforts of thousands of dedicated individuals and hundreds of companies. The Plone Foundation:

\n+

Plone is made possible only through the efforts of thousands of dedicated individuals and hundreds of companies. The\n+ Plone Foundation:

\n
    \n-
  • \xe2\x80\xa6protects and promotes Plone.
  • \n-
  • \xe2\x80\xa6is a registered 501(c)(3) charitable organization.
  • \n-
  • \xe2\x80\xa6donations are tax-deductible.
  • \n+
  • \xe2\x80\xa6protects and promotes Plone.
  • \n+
  • \xe2\x80\xa6is a registered 501(c)(3) charitable organization.
  • \n+
  • \xe2\x80\xa6donations are tax-deductible.
  • \n
\n

Thanks for using our product; we hope you like it!

\n

\xe2\x80\x94The Plone Team

\n """.format(uid=self.UID)\n- self._assertTransformsTo(text, text_out)\n+ # self._assertTransformsTo(text, text_out)\n \n def test_parsing_with_nonexisting_srcset(self):\n text = """\n' + +Repository: plone.outputfilters + + +Branch: refs/heads/master +Date: 2022-06-03T19:45:03+03:00 +Author: MrTango (MrTango) +Commit: https://github.com/plone/plone.outputfilters/commit/25c15cdc85e17834ca59e4e44d6091c553894dda + +move utils.Img2PictureTag from outputfilter to namedfile.picture + +Files changed: +M plone/outputfilters/filters/image_srcset.py +D plone/outputfilters/utils.py + +b'diff --git a/plone/outputfilters/filters/image_srcset.py b/plone/outputfilters/filters/image_srcset.py\nindex c837c07..092f90c 100644\n--- a/plone/outputfilters/filters/image_srcset.py\n+++ b/plone/outputfilters/filters/image_srcset.py\n@@ -5,7 +5,7 @@\n from plone.outputfilters.interfaces import IFilter\n from Products.CMFPlone.utils import safe_nativestring\n from zope.interface import implementer\n-from plone.outputfilters.utils import Img2PictureTag\n+from plone.namedfile.picture import Img2PictureTag\n \n logger = logging.getLogger("plone.outputfilter.image_srcset")\n \ndiff --git a/plone/outputfilters/utils.py b/plone/outputfilters/utils.py\ndeleted file mode 100644\nindex 22038ad..0000000\n--- a/plone/outputfilters/utils.py\n+++ /dev/null\n@@ -1,90 +0,0 @@\n-import logging\n-\n-from plone.base.interfaces import IImagingSchema\n-from plone.registry.interfaces import IRegistry\n-from zope.component import getUtility\n-from bs4 import BeautifulSoup\n-\n-logger = logging.getLogger("plone.outputfilter.image_srcset")\n-\n-\n-class Img2PictureTag(object):\n- @property\n- def allowed_scales(self):\n- registry = getUtility(IRegistry)\n- settings = registry.forInterface(IImagingSchema, prefix="plone", check=False)\n- return settings.allowed_sizes\n-\n- @property\n- def image_srcsets(self):\n- registry = getUtility(IRegistry)\n- settings = registry.forInterface(IImagingSchema, prefix="plone", check=False)\n- return settings.image_srcsets\n-\n- def get_scale_name(self, scale_line):\n- parts = scale_line.split(" ")\n- return parts and parts[0] or ""\n-\n- def get_scale_width(self, scale):\n- """get width from allowed_scales line\n- large 800:65536\n- """\n- for s in self.allowed_scales:\n- parts = s.split(" ")\n- if not parts:\n- continue\n- if parts[0] == scale:\n- dimentions = parts[1].split(":")\n- if not dimentions:\n- continue\n- return dimentions[0]\n-\n- def create_picture_tag(self, sourceset, attributes):\n- """Converts the element to a srcset definition"""\n- soup = BeautifulSoup("", "html.parser")\n- allowed_scales = self.allowed_scales\n- src = attributes.get("src")\n- picture_tag = soup.new_tag("picture")\n- css_classes = attributes.get("class")\n- if "captioned" in css_classes:\n- picture_tag["class"] = "captioned"\n- for i, source in enumerate(sourceset):\n- target_scale = source["scale"]\n- media = source.get("media")\n-\n- additional_scales = source.get("additionalScales", None)\n- if additional_scales is None:\n- additional_scales = [\n- self.get_scale_name(s) for s in allowed_scales if s != target_scale\n- ]\n- source_scales = [target_scale] + additional_scales\n- source_srcset = []\n- for scale in source_scales:\n- scale_url = self.update_src_scale(src=src, scale=scale)\n- scale_width = self.get_scale_width(scale)\n- source_srcset.append("{0} {1}w".format(scale_url, scale_width))\n- source_tag = soup.new_tag("source", srcset=",\\n".join(source_srcset))\n- if media:\n- source_tag["media"] = media\n- picture_tag.append(source_tag)\n- if i == len(sourceset) - 1:\n- img_tag = soup.new_tag(\n- "img",\n- src=self.update_src_scale(src=src, scale=target_scale),\n- )\n- for k, attr in attributes.items():\n- if k in ["src", "srcset"]:\n- continue\n- img_tag.attrs[k] = attr\n- img_tag["loading"] = "lazy"\n- picture_tag.append(img_tag)\n- return picture_tag\n-\n- def update_src_scale(self, src, scale):\n- parts = src.split("/")\n- if "." in parts[-1]:\n- field_name = parts[-1].split("-")[0]\n- src_scale = "/".join(parts[:-1]) + "/{0}/{1}".format(field_name, scale)\n- return src_scale\n- src_scale = "/".join(parts[:-1]) + "/{}".format(scale)\n- return src_scale\n' + +Repository: plone.outputfilters + + +Branch: refs/heads/master +Date: 2022-06-03T19:45:21+03:00 +Author: MrTango (MrTango) +Commit: https://github.com/plone/plone.outputfilters/commit/5befed3a8d8e8c788cb3a237e237a0f310e1b8cb + +prettify soup output + +Files changed: +M plone/outputfilters/filters/image_srcset.py + +b'diff --git a/plone/outputfilters/filters/image_srcset.py b/plone/outputfilters/filters/image_srcset.py\nindex 092f90c..b6cdd6f 100644\n--- a/plone/outputfilters/filters/image_srcset.py\n+++ b/plone/outputfilters/filters/image_srcset.py\n@@ -57,4 +57,4 @@ def __call__(self, data):\n if not sourceset:\n continue\n elem.replace_with(self.img2picturetag.create_picture_tag(sourceset, elem.attrs))\n- return str(soup)\n+ return soup.prettify()\n' + +Repository: plone.outputfilters + + +Branch: refs/heads/master +Date: 2022-06-03T19:46:14+03:00 +Author: MrTango (MrTango) +Commit: https://github.com/plone/plone.outputfilters/commit/76705bb3a09e06f5479e13351b66daf1b0172a1a + +cleanup + +Files changed: +M plone/outputfilters/filters/resolveuid_and_caption.py +M plone/outputfilters/tests/test_resolveuid_and_caption.py + +b'diff --git a/plone/outputfilters/filters/resolveuid_and_caption.py b/plone/outputfilters/filters/resolveuid_and_caption.py\nindex 3725e1a..a46b776 100644\n--- a/plone/outputfilters/filters/resolveuid_and_caption.py\n+++ b/plone/outputfilters/filters/resolveuid_and_caption.py\n@@ -1,6 +1,7 @@\n # -*- coding: utf-8 -*-\n from Acquisition import aq_acquire\n from Acquisition import aq_base\n+from Acquisition import aq_inner\n from Acquisition import aq_parent\n from bs4 import BeautifulSoup\n from DocumentTemplate.html_quote import html_quote\n@@ -29,13 +30,6 @@\n import six\n \n \n-HAS_LINGUAPLONE = True\n-try:\n- from Products.LinguaPlone.utils import translated_references\n-except ImportError:\n- HAS_LINGUAPLONE = False\n-\n-\n appendix_re = re.compile(\'^(.*)([?#].*)$\')\n resolveuid_re = re.compile(\'^[./]*resolve[Uu]id/([^/]*)/?(.*)$\')\n \n@@ -146,7 +140,6 @@ def _render_resolveuid(self, href):\n def __call__(self, data):\n data = re.sub(r\'<([^<>\\s]+?)\\s*/>\', self._shorttag_replace, data)\n soup = BeautifulSoup(safe_unicode(data), \'html.parser\')\n-\n for elem in soup.find_all([\'a\', \'area\']):\n attributes = elem.attrs\n href = attributes.get(\'href\')\n@@ -192,10 +185,9 @@ def __call__(self, data):\n # we could get the width/height (aspect ratio) without the scale\n # from the image field: width, height = fullimage.get("image").getImageSize()\n # XXX: refacture resolve_image to not create scales\n- if not image:\n- return\n- attributes["width"] = image.width\n- attributes["height"] = image.height\n+ if image and hasattr(image, "width"):\n+ attributes["width"] = image.width\n+ attributes["height"] = image.height\n if fullimage is not None:\n # Check to see if the alt / title tags need setting\n title = safe_unicode(aq_acquire(fullimage, \'Title\')())\n@@ -204,7 +196,6 @@ def __call__(self, data):\n attributes[\'alt\'] = description or ""\n if \'title\' not in attributes:\n attributes[\'title\'] = title\n-\n for picture_elem in soup.find_all(\'picture\'):\n if \'captioned\' not in picture_elem.attrs.get(\'class\', []):\n continue\n@@ -237,16 +228,6 @@ def __call__(self, data):\n \n return six.text_type(soup)\n \n- def lookup_uid(self, uid):\n- context = self.context\n- if HAS_LINGUAPLONE:\n- # If we have LinguaPlone installed, add support for language-aware\n- # references\n- uids = translated_references(context, context.Language(), uid)\n- if len(uids) > 0:\n- uid = uids[0]\n- return uuidToObject(uid)\n-\n def resolve_scale_data(self, url):\n """ return scale url, width and height\n """\n@@ -271,7 +252,7 @@ def resolve_link(self, href):\n match = resolveuid_re.match(subpath)\n if match is not None:\n uid, _subpath = match.groups()\n- obj = self.lookup_uid(uid)\n+ obj = uuidToObject(uid)\n if obj is not None:\n subpath = _subpath\n \ndiff --git a/plone/outputfilters/tests/test_resolveuid_and_caption.py b/plone/outputfilters/tests/test_resolveuid_and_caption.py\nindex cb011bd..c9b19b8 100644\n--- a/plone/outputfilters/tests/test_resolveuid_and_caption.py\n+++ b/plone/outputfilters/tests/test_resolveuid_and_caption.py\n@@ -334,7 +334,7 @@ def test_image_captioning_in_news_item(self):\n # Test captioning\n output = news_item.text.output\n text_out = """
\n-My caption\n+\n
My caption
\n
\n
"""\n@@ -354,7 +354,7 @@ def test_image_captioning_absolute_path(self):\n self._assertTransformsTo(text_in, text_out)\n \n def test_image_captioning_relative_path(self):\n- text_in = """"""\n+ text_in = """"""\n text_out = """
\n My caption\n
My caption
\n@@ -423,7 +423,7 @@ def test_image_captioning_resolveuid_new_scale_plone_namedfile(self):\n def test_image_captioning_resolveuid_no_scale(self):\n text_in = """""" % self.UID\n text_out = """
\n-My caption\n+My caption\n
My caption
\n
"""\n self._assertTransformsTo(text_in, text_out)\n@@ -431,7 +431,7 @@ def test_image_captioning_resolveuid_no_scale(self):\n def test_image_captioning_resolveuid_with_srcset_and_src(self):\n text_in = """""" % (self.UID, self.UID, self.UID)\n text_out = """
\n-My caption\n+My caption\n
My caption
\n
"""\n self._assertTransformsTo(text_in, text_out)\n' + +Repository: plone.outputfilters + + +Branch: refs/heads/master +Date: 2022-06-03T19:46:34+03:00 +Author: MrTango (MrTango) +Commit: https://github.com/plone/plone.outputfilters/commit/f734494ec19009c8f157a48f83df083f1b17814e + +fix test + +Files changed: +M plone/outputfilters/tests/test_resolveuid_and_caption.py + +b'diff --git a/plone/outputfilters/tests/test_resolveuid_and_caption.py b/plone/outputfilters/tests/test_resolveuid_and_caption.py\nindex c9b19b8..e2dfc51 100644\n--- a/plone/outputfilters/tests/test_resolveuid_and_caption.py\n+++ b/plone/outputfilters/tests/test_resolveuid_and_caption.py\n@@ -327,7 +327,7 @@ def test_image_captioning_in_news_item(self):\n news_item = self.portal[\'a-news-item\']\n from plone.app.textfield.value import RichTextValue\n news_item.text = RichTextValue(\n- \'\',\n+ \'\',\n \'text/html\', \'text/x-html-safe\')\n news_item.setDescription("Description.")\n \n' + +Repository: plone.outputfilters + + +Branch: refs/heads/master +Date: 2022-06-07T17:26:22+03:00 +Author: MrTango (MrTango) +Commit: https://github.com/plone/plone.outputfilters/commit/be09d6a5a964933d62e75c209238857305d0ff5d + +rename image_srcset to picture_variants, fix captioning + +Files changed: +A plone/outputfilters/filters/picture_variants.py +A plone/outputfilters/tests/test_picture_variants.py +M plone/outputfilters/filters/configure.zcml +M plone/outputfilters/filters/resolveuid_and_caption.py +M plone/outputfilters/tests/test_resolveuid_and_caption.py +D plone/outputfilters/filters/image_srcset.py +D plone/outputfilters/tests/test_image_srcset.py + +b'diff --git a/plone/outputfilters/filters/configure.zcml b/plone/outputfilters/filters/configure.zcml\nindex c351214..4376517 100644\n--- a/plone/outputfilters/filters/configure.zcml\n+++ b/plone/outputfilters/filters/configure.zcml\n@@ -5,9 +5,9 @@\n \n \n \n Some simple text.\'\n+ text = """
\n+ Some simple text.\n+
"""\n res = self.parser(text)\n self.assertEqual(text, str(res))\n \n@@ -122,7 +124,7 @@ def test_parsing_long_doc(self):\n

If you\'re seeing this instead of the web site you were expecting, the owner of this web site has\n just installed Plone. Do not contact the Plone Team or the Plone support channels about this.

\n

\n

Get started

\n

Before you start exploring your newly created Plone site, please do the following:

\n@@ -133,7 +135,7 @@ def test_parsing_long_doc(self):\n

Get comfortable

\n

After that, we suggest you do one or more of the following:

\n

\n

Make it your own

\n

Plone has a lot of different settings that can be used to make it do what you want it to. Some examples:

\n@@ -142,11 +144,11 @@ def test_parsing_long_doc(self):\n that delivers Plone-based solutions?

\n

Find out more about Plone

\n

\n+ alt="" data-linktype="image" data-picturevariant="large" data-scale="huge" data-val="{uid}" />

\n

Plone is a powerful content management system built on a rock-solid application stack written using the Python\n programming language. More about these technologies:

\n

\n+ data-linktype="image" data-picturevariant="large" data-scale="huge" data-val="{uid}" />\n

Support the Plone Foundation

\n

Plone is made possible only through the efforts of thousands of dedicated individuals and hundreds of companies. The\n Plone Foundation:

\n@@ -171,10 +173,10 @@ def test_parsing_long_doc(self):\n

\n \n \n- \n+ srcset="resolveuid/{uid}/@@images/image/preview 400w, resolveuid/{uid}/@@images/image/preview 400w, resolveuid/{uid}/@@images/image/large 800w, resolveuid/{uid}/@@images/image/larger 1000w"/>\n+ \n \n

\n

Get started

\n@@ -188,10 +190,10 @@ def test_parsing_long_doc(self):\n

\n \n \n+ srcset="resolveuid/{uid}/@@images/image/teaser 600w, resolveuid/{uid}/@@images/image/preview 400w, resolveuid/{uid}/@@images/image/large 800w, resolveuid/{uid}/@@images/image/larger 1000w, resolveuid/{uid}/@@images/image/great 1200w"/>\n \n+ data-picturevariant="medium" data-scale="larger" data-val="{uid}" loading="lazy"\n+ src="resolveuid/{uid}/@@images/image/teaser"/>\n \n

\n

Make it your own

\n@@ -203,10 +205,10 @@ def test_parsing_long_doc(self):\n

\n \n \n- \n+ srcset="resolveuid/{uid}/@@images/image/larger 1000w, resolveuid/{uid}/@@images/image/preview 400w, resolveuid/{uid}/@@images/image/teaser 600w, resolveuid/{uid}/@@images/image/large 800w, resolveuid/{uid}/@@images/image/great 1200w, resolveuid/{uid}/@@images/image/huge 1600w"/>\n+ \n \n

\n

Plone is a powerful content management system built on a rock-solid application stack written using the Python\n@@ -214,10 +216,10 @@ def test_parsing_long_doc(self):\n

\n \n \n- \n+ srcset="resolveuid/{uid}/@@images/image/larger 1000w, resolveuid/{uid}/@@images/image/preview 400w, resolveuid/{uid}/@@images/image/teaser 600w, resolveuid/{uid}/@@images/image/large 800w, resolveuid/{uid}/@@images/image/great 1200w, resolveuid/{uid}/@@images/image/huge 1600w"/>\n+ \n \n

\n

Support the Plone Foundation

\n@@ -231,16 +233,16 @@ def test_parsing_long_doc(self):\n

Thanks for using our product; we hope you like it!

\n

\xe2\x80\x94The Plone Team

\n """.format(uid=self.UID)\n- # self._assertTransformsTo(text, text_out)\n+ self._assertTransformsTo(text, text_out)\n \n def test_parsing_with_nonexisting_srcset(self):\n text = """\n-

\n+

\n """.format(uid=self.UID)\n res = self.parser(text)\n self.assertTrue(res)\n text_out = """\n-

\n+

\n """.format(uid=self.UID)\n # verify that tag was not converted:\n- self.assertTrue("data-srcset" in res)\n\\ No newline at end of file\n+ self.assertTrue("data-picturevariant" in res)\n\\ No newline at end of file\ndiff --git a/plone/outputfilters/tests/test_resolveuid_and_caption.py b/plone/outputfilters/tests/test_resolveuid_and_caption.py\nindex e2dfc51..5724a6d 100644\n--- a/plone/outputfilters/tests/test_resolveuid_and_caption.py\n+++ b/plone/outputfilters/tests/test_resolveuid_and_caption.py\n@@ -72,14 +72,17 @@ def UID(self):\n self.portal._setObject(\'foo2\', dummy2)\n self.portal.portal_catalog.catalog_object(self.portal.foo2)\n \n- def _assertTransformsTo(self, input, expected):\n+ def _assertTransformsTo(self, input, expected, parsing=True):\n # compare two chunks of HTML ignoring whitespace differences,\n # and with a useful diff on failure\n- out = self.parser(input)\n+ if parsing:\n+ out = self.parser(input)\n+ else:\n+ out = input\n normalized_out = normalize_html(out)\n normalized_expected = normalize_html(expected)\n- # print("e: {}".format(normalized_expected))\n- # print("o: {}".format(normalized_out))\n+ print("e: {}".format(normalized_expected))\n+ print("o: {}".format(normalized_out))\n try:\n self.assertTrue(_ellipsis_match(normalized_expected,\n normalized_out))\n@@ -327,10 +330,9 @@ def test_image_captioning_in_news_item(self):\n news_item = self.portal[\'a-news-item\']\n from plone.app.textfield.value import RichTextValue\n news_item.text = RichTextValue(\n- \'\',\n- \'text/html\', \'text/x-html-safe\')\n+ \'\',\n+ \'text/html\', \'text/html\')\n news_item.setDescription("Description.")\n-\n # Test captioning\n output = news_item.text.output\n text_out = """
\n' + +Repository: plone.outputfilters + + +Branch: refs/heads/master +Date: 2022-06-08T16:09:28+03:00 +Author: MrTango (MrTango) +Commit: https://github.com/plone/plone.outputfilters/commit/bc7647b44b284a763a3259f9f9c1c9e494f78502 + +refacture use of Img2PictureTag/get_picture_variants, and fix tests + +Files changed: +M plone/outputfilters/filters/picture_variants.py +M plone/outputfilters/filters/resolveuid_and_caption.py +M plone/outputfilters/tests/test_resolveuid_and_caption.py + +b'diff --git a/plone/outputfilters/filters/picture_variants.py b/plone/outputfilters/filters/picture_variants.py\nindex 36e62ff..935c3d2 100644\n--- a/plone/outputfilters/filters/picture_variants.py\n+++ b/plone/outputfilters/filters/picture_variants.py\n@@ -5,7 +5,7 @@\n from plone.outputfilters.interfaces import IFilter\n from Products.CMFPlone.utils import safe_nativestring\n from zope.interface import implementer\n-from plone.namedfile.picture import Img2PictureTag\n+from plone.namedfile.picture import Img2PictureTag, get_picture_variants\n \n logger = logging.getLogger("plone.outputfilter.picture_variants")\n \n@@ -45,7 +45,7 @@ def __call__(self, data):\n picture_variant_name = elem.attrs.get("data-picturevariant", "")\n if not picture_variant_name:\n continue\n- picture_variants_config = self.img2picturetag.picture_variants.get(picture_variant_name)\n+ picture_variants_config = get_picture_variants().get(picture_variant_name)\n if not picture_variants_config:\n logger.warn(\n "Could not find the given picture_variant_name {0}, leave tag untouched!".format(\ndiff --git a/plone/outputfilters/filters/resolveuid_and_caption.py b/plone/outputfilters/filters/resolveuid_and_caption.py\nindex 36c194f..6ddd076 100644\n--- a/plone/outputfilters/filters/resolveuid_and_caption.py\n+++ b/plone/outputfilters/filters/resolveuid_and_caption.py\n@@ -227,6 +227,8 @@ def __call__(self, data):\n captioned.a.unwrap()\n if elem.name == "picture":\n del captioned.picture.img["class"]\n+ else:\n+ del captioned.img["class"]\n elem.replace_with(captioned)\n \n # # captioning must hapen before resolving uid\'s\ndiff --git a/plone/outputfilters/tests/test_resolveuid_and_caption.py b/plone/outputfilters/tests/test_resolveuid_and_caption.py\nindex 5724a6d..d0a5f3f 100644\n--- a/plone/outputfilters/tests/test_resolveuid_and_caption.py\n+++ b/plone/outputfilters/tests/test_resolveuid_and_caption.py\n@@ -336,7 +336,7 @@ def test_image_captioning_in_news_item(self):\n # Test captioning\n output = news_item.text.output\n text_out = """
\n-\n+\n
My caption
\n
\n
"""\n@@ -344,13 +344,13 @@ def test_image_captioning_in_news_item(self):\n \n def test_image_captioning_absolutizes_uncaptioned_image(self):\n text_in = """"""\n- text_out = """My caption"""\n+ text_out = """"""\n self._assertTransformsTo(text_in, text_out)\n \n def test_image_captioning_absolute_path(self):\n text_in = """"""\n text_out = """
\n-My caption\n+\n
My caption
\n
"""\n self._assertTransformsTo(text_in, text_out)\n@@ -358,7 +358,7 @@ def test_image_captioning_absolute_path(self):\n def test_image_captioning_relative_path(self):\n text_in = """"""\n text_out = """
\n-My caption\n+\n
My caption
\n
"""\n self._assertTransformsTo(text_in, text_out)\n@@ -379,7 +379,7 @@ def test_image_captioning_relative_path_private_folder(self):\n \n text_in = """"""\n text_out = """
\n-My private image caption\n+\n
My private image caption
\n
"""\n self._assertTransformsTo(text_in, text_out)\n@@ -387,7 +387,7 @@ def test_image_captioning_relative_path_private_folder(self):\n def test_image_captioning_relative_path_scale(self):\n text_in = """"""\n text_out = """
\n-My caption\n+\n
My caption
\n
"""\n self._assertTransformsTo(text_in, text_out)\n@@ -395,7 +395,7 @@ def test_image_captioning_relative_path_scale(self):\n def test_image_captioning_resolveuid(self):\n text_in = """""" % self.UID\n text_out = """
\n-My caption\n+\n
My caption
\n
"""\n self._assertTransformsTo(text_in, text_out)\n@@ -403,7 +403,7 @@ def test_image_captioning_resolveuid(self):\n def test_image_captioning_resolveuid_scale(self):\n text_in = """""" % self.UID\n text_out = """
\n-My caption\n+\n
My caption
\n
"""\n self._assertTransformsTo(text_in, text_out)\n@@ -411,7 +411,7 @@ def test_image_captioning_resolveuid_scale(self):\n def test_image_captioning_resolveuid_new_scale(self):\n text_in = """""" % self.UID\n text_out = """
\n-My caption\n+\n
My caption
\n
"""\n self._assertTransformsTo(text_in, text_out)\n@@ -419,13 +419,13 @@ def test_image_captioning_resolveuid_new_scale(self):\n def test_image_captioning_resolveuid_new_scale_plone_namedfile(self):\n self._makeDummyContent()\n text_in = """"""\n- text_out = u"""Sch\xc3\xb6nes Bild"""\n+ text_out = u""""""\n self._assertTransformsTo(text_in, text_out)\n \n def test_image_captioning_resolveuid_no_scale(self):\n text_in = """""" % self.UID\n text_out = """
\n-My caption\n+\n
My caption
\n
"""\n self._assertTransformsTo(text_in, text_out)\n@@ -433,7 +433,7 @@ def test_image_captioning_resolveuid_no_scale(self):\n def test_image_captioning_resolveuid_with_srcset_and_src(self):\n text_in = """""" % (self.UID, self.UID, self.UID)\n text_out = """
\n-My caption\n+\n
My caption
\n
"""\n self._assertTransformsTo(text_in, text_out)\n@@ -466,7 +466,7 @@ def test_source_resolveuid_srcset(self):\n def test_image_captioning_resolveuid_no_scale_plone_namedfile(self):\n self._makeDummyContent()\n text_in = """"""\n- text_out = u"""Sch\xc3\xb6nes Bild"""\n+ text_out = u""""""\n self._assertTransformsTo(text_in, text_out)\n \n def test_image_captioning_bad_uid(self):\n@@ -488,7 +488,7 @@ def test_image_captioning_external_url(self):\n def test_image_captioning_preserves_custom_attributes(self):\n text_in = """"""\n text_out = """
\n-My caption\n+\n
My caption
\n
"""\n self._assertTransformsTo(text_in, text_out)\n@@ -504,7 +504,7 @@ def test_image_captioning_handles_unquoted_attributes(self):\n def test_image_captioning_preserves_existing_links(self):\n text_in = """"""\n text_out = """
\n-My caption\n+\n
My caption
\n
\n
"""\n' + +Repository: plone.outputfilters + + +Branch: refs/heads/master +Date: 2022-06-08T15:55:01+02:00 Author: Maurits van Rees (mauritsvanrees) -Commit: https://github.com/plone/Products.CMFPlone/commit/28d1ea602ee0d6299bb5b90c24de7be2fa10347a +Commit: https://github.com/plone/plone.outputfilters/commit/c02dbb98de026ae07d58b8287b5463200b4a06c5 + +Fixed getting width and height for img with resolveuid without @@images. + +Try @@images/image in that case. +Renamed test_image_captioning_resolveuid to test_image_captioning_resolveuid_bare so you can run this test on its own. + +Files changed: +M plone/outputfilters/filters/resolveuid_and_caption.py +M plone/outputfilters/tests/test_resolveuid_and_caption.py + +b'diff --git a/plone/outputfilters/filters/resolveuid_and_caption.py b/plone/outputfilters/filters/resolveuid_and_caption.py\nindex 6ddd076..148471a 100644\n--- a/plone/outputfilters/filters/resolveuid_and_caption.py\n+++ b/plone/outputfilters/filters/resolveuid_and_caption.py\n@@ -349,7 +349,11 @@ def traverse_path(base, path):\n if obj is not None:\n # resolved uid\n fullimage = obj\n- image = traverse_path(fullimage, subpath)\n+ image = None\n+ if not subpath:\n+ image = traverse_path(fullimage, "@@images/image")\n+ if image is None:\n+ image = traverse_path(fullimage, subpath)\n elif \'/@@\' in subpath:\n # split on view\n pos = subpath.find(\'/@@\')\ndiff --git a/plone/outputfilters/tests/test_resolveuid_and_caption.py b/plone/outputfilters/tests/test_resolveuid_and_caption.py\nindex d0a5f3f..3331ffb 100644\n--- a/plone/outputfilters/tests/test_resolveuid_and_caption.py\n+++ b/plone/outputfilters/tests/test_resolveuid_and_caption.py\n@@ -392,7 +392,7 @@ def test_image_captioning_relative_path_scale(self):\n
"""\n self._assertTransformsTo(text_in, text_out)\n \n- def test_image_captioning_resolveuid(self):\n+ def test_image_captioning_resolveuid_bare(self):\n text_in = """""" % self.UID\n text_out = """
\n \n' + +Repository: plone.outputfilters + + +Branch: refs/heads/master +Date: 2022-06-08T17:26:29+03:00 +Author: MrTango (MrTango) +Commit: https://github.com/plone/plone.outputfilters/commit/fc8cae069a32d938a069366f0ae7d95d89fb8a88 + +fix test_image_captioning_relative_path_private_folder + +Files changed: +M plone/outputfilters/filters/resolveuid_and_caption.py + +b'diff --git a/plone/outputfilters/filters/resolveuid_and_caption.py b/plone/outputfilters/filters/resolveuid_and_caption.py\nindex 148471a..014cd14 100644\n--- a/plone/outputfilters/filters/resolveuid_and_caption.py\n+++ b/plone/outputfilters/filters/resolveuid_and_caption.py\n@@ -181,7 +181,6 @@ def __call__(self, data):\n img_elem = elem.find("img")\n else:\n img_elem = elem\n-\n # handle src attribute\n attributes = img_elem.attrs\n src = attributes.get(\'src\', \'\')\n@@ -230,41 +229,6 @@ def __call__(self, data):\n else:\n del captioned.img["class"]\n elem.replace_with(captioned)\n-\n- # # captioning must hapen before resolving uid\'s\n- # for cap_elem in soup.find_all([\'picture\', \'img\']):\n- # if \'captioned\' in cap_elem.attrs.get(\'class\', []):\n- # if cap_elem.name == "picture":\n- # img_elem = cap_elem.find("img")\n- # else:\n- # img_elem = cap_elem\n- # attributes = img_elem.attrs\n- # src = attributes.get(\'src\', \'\')\n- # image, fullimage, src, description = self.resolve_image(src)\n- # attributes["src"] = src\n- # caption = description\n- # caption_manual_override = attributes.get("data-captiontext", "")\n- # if caption_manual_override:\n- # caption = caption_manual_override\n- # # Check if the image needs to be captioned\n- # if (self.captioned_images and caption):\n- # options = {}\n- # options["tag"] = cap_elem.prettify()\n- # options["caption"] = newline_to_br(html_quote(caption))\n- # options["class"] = \' \'.join(attributes[\'class\'])\n- # del attributes[\'class\']\n- # if cap_elem.name == "picture":\n- # cap_elem.append(img_elem)\n- # captioned = BeautifulSoup(\n- # self.captioned_image_template(**options), \'html.parser\')\n-\n- # # if we are a captioned image within a link, remove and occurrences\n- # # of a tags inside caption template to preserve the outer link\n- # if bool(cap_elem.find_parent(\'a\')) and bool(captioned.find(\'a\')):\n- # captioned.a.unwrap()\n- # if cap_elem.name == "picture":\n- # del captioned.picture.img["class"]\n- # cap_elem.replace_with(captioned)\n return six.text_type(soup)\n \n def resolve_scale_data(self, url):\n@@ -374,6 +338,10 @@ def traverse_path(base, path):\n if hasattr(aq_base(parent), \'tag\'):\n fullimage = parent\n break\n+ if not hasattr(image, "width"):\n+ image_scale = traverse_path(image, "@@images/image")\n+ if image_scale:\n+ image = image_scale\n \n if image is None:\n return None, None, src, description\n' + +Repository: plone.outputfilters + + +Branch: refs/heads/master +Date: 2022-06-08T17:27:06+03:00 +Author: MrTango (MrTango) +Commit: https://github.com/plone/plone.outputfilters/commit/0ffd8535c21153b17f837e98d157f62a31c0a855 + +remove wrong tests, srcset is not allowed in video/audio + +Files changed: +M plone/outputfilters/tests/test_resolveuid_and_caption.py + +b'diff --git a/plone/outputfilters/tests/test_resolveuid_and_caption.py b/plone/outputfilters/tests/test_resolveuid_and_caption.py\nindex 3331ffb..51c32d2 100644\n--- a/plone/outputfilters/tests/test_resolveuid_and_caption.py\n+++ b/plone/outputfilters/tests/test_resolveuid_and_caption.py\n@@ -453,16 +453,6 @@ def test_audio_resolveuid(self):\n text_out = """"""\n self._assertTransformsTo(text_in, text_out)\n \n- def test_source_resolveuid(self):\n- text_in = """""" % self.UID\n- text_out = """"""\n- self._assertTransformsTo(text_in, text_out)\n-\n- def test_source_resolveuid_srcset(self):\n- text_in = """""" % self.UID\n- text_out = """"""\n- self._assertTransformsTo(text_in, text_out)\n-\n def test_image_captioning_resolveuid_no_scale_plone_namedfile(self):\n self._makeDummyContent()\n text_in = """"""\n' + +Repository: plone.outputfilters + + +Branch: refs/heads/master +Date: 2022-06-08T17:41:07+03:00 +Author: MrTango (MrTango) +Commit: https://github.com/plone/plone.outputfilters/commit/8e23b0a9097aad3d55887f0deb6797410ec95d54 + +fix test + +Files changed: +M plone/outputfilters/README.rst +M plone/outputfilters/tests/test_resolveuid_and_caption.py + +b'diff --git a/plone/outputfilters/README.rst b/plone/outputfilters/README.rst\nindex 454b166..31342f9 100644\n--- a/plone/outputfilters/README.rst\n+++ b/plone/outputfilters/README.rst\n@@ -62,7 +62,7 @@ be applied::\n >>> portal = layer[\'portal\']\n >>> str(portal.portal_transforms.convertTo(\'text/x-html-safe\',\n ... \'test--test\', mimetype=\'text/html\', context=portal))\n- \'test\xe2\x80\x94test\'\n+ \'test\xe2\x80\x94test\\n\'\n \n \n How it works\ndiff --git a/plone/outputfilters/tests/test_resolveuid_and_caption.py b/plone/outputfilters/tests/test_resolveuid_and_caption.py\nindex 51c32d2..89a42d9 100644\n--- a/plone/outputfilters/tests/test_resolveuid_and_caption.py\n+++ b/plone/outputfilters/tests/test_resolveuid_and_caption.py\n@@ -344,7 +344,7 @@ def test_image_captioning_in_news_item(self):\n \n def test_image_captioning_absolutizes_uncaptioned_image(self):\n text_in = """"""\n- text_out = """"""\n+ text_out = """"""\n self._assertTransformsTo(text_in, text_out)\n \n def test_image_captioning_absolute_path(self):\n@@ -356,7 +356,7 @@ def test_image_captioning_absolute_path(self):\n self._assertTransformsTo(text_in, text_out)\n \n def test_image_captioning_relative_path(self):\n- text_in = """"""\n+ text_in = """"""\n text_out = """
\n \n
My caption
\n@@ -506,7 +506,7 @@ def test_image_captioning_handles_non_ascii(self):\n u\'Kupu Test Image \\xe5\\xe4\\xf6\')\n text_in = """"""\n text_out = u"""
\n-Kupu Test Image \\xe5\\xe4\\xf6\n+\n
Kupu Test Image \\xe5\\xe4\\xf6
\n
"""\n self._assertTransformsTo(text_in, text_out)\n' + +Repository: plone.outputfilters + + +Branch: refs/heads/master +Date: 2022-06-08T17:43:44+03:00 +Author: MrTango (MrTango) +Commit: https://github.com/plone/plone.outputfilters/commit/bf3670e6429446749fdba39b83405fbef982b5d9 + +fix last test + +Files changed: +M plone/outputfilters/tests/test_resolveuid_and_caption.py + +b'diff --git a/plone/outputfilters/tests/test_resolveuid_and_caption.py b/plone/outputfilters/tests/test_resolveuid_and_caption.py\nindex 89a42d9..f011457 100644\n--- a/plone/outputfilters/tests/test_resolveuid_and_caption.py\n+++ b/plone/outputfilters/tests/test_resolveuid_and_caption.py\n@@ -344,7 +344,7 @@ def test_image_captioning_in_news_item(self):\n \n def test_image_captioning_absolutizes_uncaptioned_image(self):\n text_in = """"""\n- text_out = """"""\n+ text_out = """"""\n self._assertTransformsTo(text_in, text_out)\n \n def test_image_captioning_absolute_path(self):\n' + +Repository: plone.outputfilters + + +Branch: refs/heads/master +Date: 2022-06-08T17:46:23+03:00 +Author: MrTango (MrTango) +Commit: https://github.com/plone/plone.outputfilters/commit/283332a82f3c6e3cee011055651c4577bea5049c -Merge branch 'master' into mrtango-image-handling-sourcesets-settings +fix it again ;) Files changed: -A news/3528.feature -A news/3539.bugfix -M Products/CMFPlone/browser/admin.py -M Products/CMFPlone/browser/templates/plone-upgrade.pt -M Products/CMFPlone/tests/robot/test_controlpanel_actions.robot -M Products/CMFPlone/tests/robot/test_controlpanel_filter.robot -M Products/CMFPlone/tests/robot/test_controlpanel_navigation.robot -M Products/CMFPlone/tests/robot/test_controlpanel_search.robot -M Products/CMFPlone/tests/robot/test_controlpanel_security.robot -M Products/CMFPlone/tests/robot/test_controlpanel_site.robot -M Products/CMFPlone/tests/robot/test_controlpanel_social.robot -M Products/CMFPlone/tests/robot/test_edit_user_schema.robot -M Products/CMFPlone/tests/robot/test_linkintegrity.robot -M Products/CMFPlone/tests/robot/test_livesearch.robot -M Products/CMFPlone/tests/robot/test_querystring.robot -M Products/CMFPlone/tests/robot/test_tinymce.robot -M Products/CMFPlone/tests/testResourceRegistries.py +M plone/outputfilters/tests/test_resolveuid_and_caption.py -b'diff --git a/Products/CMFPlone/browser/admin.py b/Products/CMFPlone/browser/admin.py\nindex dfa3df808c..972a6c4789 100644\n--- a/Products/CMFPlone/browser/admin.py\n+++ b/Products/CMFPlone/browser/admin.py\n@@ -343,3 +343,16 @@ def __call__(self):\n )\n \n return self.index()\n+\n+ def can_migrate_to_volto(self):\n+ if not HAS_VOLTO:\n+ return False\n+ pm = getattr(self.context, \'portal_migration\')\n+ if pm.getInstanceVersion() < "6005":\n+ return False\n+ try:\n+ from plone.volto.browser import migrate_to_volto\n+ except ImportError:\n+ return False\n+ installer = get_installer(self.context, self.request)\n+ return not installer.is_product_installed("plone.volto")\ndiff --git a/Products/CMFPlone/browser/templates/plone-upgrade.pt b/Products/CMFPlone/browser/templates/plone-upgrade.pt\nindex c4a09be6dd..21b66bbe9e 100644\n--- a/Products/CMFPlone/browser/templates/plone-upgrade.pt\n+++ b/Products/CMFPlone/browser/templates/plone-upgrade.pt\n@@ -70,6 +70,16 @@\n \n \n \n+ \n+

\n+ You can prepare your site for Volto, the default frontend of Plone 6!\n+

\n+ \n+ Click here if you want to learn more.\n+ \n+
\n+\n \n
button\n- Wait Until Element Is Visible css=.pattern-modal-buttons > button\n- Click Element css=.pattern-modal-buttons > button\n+ Wait For Then Click Element css=.pattern-modal-buttons > button\n \n I change the actions order\n Click Link css=section:nth-child(3) li:first-child a\n Wait until page contains Action Settings\n Input Text for sure form.widgets.position 3\n- Set Focus To Element css=.pattern-modal-buttons > button\n- Wait Until Element Is Visible css=.pattern-modal-buttons > button\n- Click Element css=.pattern-modal-buttons > button\n+ Wait For Then Click Element css=.pattern-modal-buttons > button\n \n I add a new action\n Click Link Add new action\n Wait until page contains New action\n Select From List By Label form.widgets.category:list User actions\n Input Text for sure form.widgets.id favorites\n- Set Focus To Element css=.pattern-modal-buttons > button\n- Wait Until Element Is Visible css=.pattern-modal-buttons > button\n- Click Element css=.pattern-modal-buttons > button\n+ Wait For Then Click Element css=.pattern-modal-buttons > button\n Wait until page contains favorites\n- Set Focus To Element css=section.category:last-child li:last-child a\n- Wait Until Element Is Visible css=section.category:last-child li:last-child a\n- Sleep 1\n- Click Link css=section.category:last-child li:last-child a\n+ Wait For Then Click Element css=section.category:last-child li:last-child a\n Wait until page contains Action Settings\n Input Text for sure form.widgets.title My favorites\n Input Text for sure form.widgets.url_expr string:\\${globals_view/navigationRootUrl}/favorites\n- Set Focus To Element css=.pattern-modal-buttons > button\n- Wait Until Element Is Visible css=.pattern-modal-buttons > button\n- Click Element css=.pattern-modal-buttons > button\n+ Wait For Then Click Element css=.pattern-modal-buttons > button\n \n I delete an action\n Click Button css=section:nth-child(3) li:first-child button[name=delete]\ndiff --git a/Products/CMFPlone/tests/robot/test_controlpanel_filter.robot b/Products/CMFPlone/tests/robot/test_controlpanel_filter.robot\nindex d056bf0a8e..0bad48857c 100644\n--- a/Products/CMFPlone/tests/robot/test_controlpanel_filter.robot\n+++ b/Products/CMFPlone/tests/robot/test_controlpanel_filter.robot\n@@ -69,6 +69,12 @@ the filter control panel\n \n Input RichText\n [Arguments] ${input}\n+ # Sleep to avoid random failures where the text is not actually set.\n+ # This warning from the robotframework docs might be the cause:\n+ # "Starting from Robot Framework 2.5 errors caused by invalid syntax, timeouts,\n+ # or fatal exceptions are not caught by this keyword."\n+ # See https://robotframework.org/robotframework/2.6.1/libraries/BuiltIn.html#Wait%20Until%20Keyword%20Succeeds\n+ Sleep 1\n Wait until keyword succeeds 5s 1s Execute Javascript tinyMCE.activeEditor.setContent(\'${input}\');\n \n \n@@ -77,26 +83,24 @@ Input RichText\n I add \'${tag}\' to the nasty tags list and remove it from the valid tags list\n Input Text name=form.widgets.nasty_tags ${tag}\n Remove line from textarea form.widgets.valid_tags ${tag}\n- Click Button Save\n- Wait until page contains Changes saved\n+ I save the form\n \n I remove \'${tag}\' from the valid tags list\n Remove line from textarea form.widgets.valid_tags ${tag}\n- Click Button Save\n- Wait until page contains Changes saved\n+ I save the form\n \n I add \'${tag}\' to the valid tags list\n Input Text name=form.widgets.valid_tags ${tag}\n- Click Button Save\n- Wait until page contains Changes saved\n+ I save the form\n+ Page Should Contain ${tag}\n \n I add \'${tag}\' to the custom attributes list\n Input Text name=form.widgets.custom_attributes ${tag}\n- Click Button Save\n- Wait until page contains Changes saved\n+ I save the form\n+ Page Should Contain ${tag}\n \n I save the form\n- Click Button Save\n+ Wait For Then Click Element form.buttons.save\n Wait until page contains Changes saved\n \n \n@@ -106,18 +110,23 @@ the \'h1\' tag is filtered out when a document is saved\n ${doc1_uid}= Create content id=doc1 title=Document 1 type=Document\n Go To ${PLONE_URL}/doc1/edit\n patterns are loaded\n- Input RichText

h1 heading

lorem ipsum

\n- Click Button Save\n- Wait until page contains Changes saved\n+ Input RichText

h1 heading

Spanish Inquisition

\n+ I save the form\n+ # We check that some standard text is there, before checking the interesting part.\n+ # If the standard text is invisible, then something completely different is wrong.\n+ # I see tests randomly fail where the safe html transform is not even called.\n+ # In fact, no text is saved. Maybe some timing problem.\n+ # I suspect the Input RichText keyword, which is why I added Sleep in there.\n+ Page should contain Spanish Inquisition\n Page should not contain heading\n \n the \'h1\' tag is stripped when a document is saved\n ${doc1_uid}= Create content id=doc1 title=Document 1 type=Document\n Go To ${PLONE_URL}/doc1/edit\n patterns are loaded\n- Input RichText

h1 heading

lorem ipsum

\n- Click Button Save\n- Wait until page contains Changes saved\n+ Input RichText

h1 heading

Spanish Inquisition

\n+ I save the form\n+ Page should contain Spanish Inquisition\n Page should contain heading\n Page Should Contain Element //div[@id=\'content-core\']//h1 limit=0 message=h1 should have been stripped out\n \n@@ -125,18 +134,18 @@ the \'${tag}\' tag is preserved when a document is saved\n ${doc1_uid}= Create content id=doc1 title=Document 1 type=Document\n Go To ${PLONE_URL}/doc1/edit\n patterns are loaded\n- Input RichText <${tag}>lorem ipsum\n- Click Button Save\n- Wait until page contains Changes saved\n+ Input RichText <${tag}>lorem ipsum

Spanish Inquisition

\n+ I save the form\n+ Page should contain Spanish Inquisition\n Page Should Contain Element //div[@id=\'content-core\']//${tag} limit=1 message=the ${tag} tag should have been preserved\n \n the \'${attribute}\' attribute is preserved when a document is saved\n ${doc1_uid}= Create content id=doc1 title=Document 1 type=Document\n Go To ${PLONE_URL}/doc1/edit\n patterns are loaded\n- Input RichText lorem ipsum\n- Click Button Save\n- Wait until page contains Changes saved\n+ Input RichText lorem ipsum

Spanish Inquisition

\n+ I save the form\n+ Page should contain Spanish Inquisition\n Page Should Contain Element //span[@${attribute}] limit=1 message=the ${attribute} tag should have been preserved\n \n success message should contain information regarding caching\ndiff --git a/Products/CMFPlone/tests/robot/test_controlpanel_navigation.robot b/Products/CMFPlone/tests/robot/test_controlpanel_navigation.robot\nindex 7518cd3a03..c277aedf52 100644\n--- a/Products/CMFPlone/tests/robot/test_controlpanel_navigation.robot\n+++ b/Products/CMFPlone/tests/robot/test_controlpanel_navigation.robot\n@@ -68,37 +68,27 @@ a private document \'${title}\'\n \n I disable generate tabs\n Unselect Checkbox form.widgets.generate_tabs:list\n- Set Focus To Element form.buttons.save\n- Wait Until Element Is Visible form.buttons.save\n- Click Button Save\n+ Wait For Then Click Element form.buttons.save\n Wait until page contains Changes saved\n \n I disable non-folderish tabs\n Unselect Checkbox xpath=//input[@value=\'Document\']\n- Set Focus To Element form.buttons.save\n- Wait Until Element Is Visible form.buttons.save\n- Click Button Save\n+ Wait For Then Click Element form.buttons.save\n Wait until page contains Changes saved\n \n I remove \'${portal_type}\' from the displayed types list\n Unselect Checkbox xpath=//input[@value=\'Document\']\n- Set Focus To Element form.buttons.save\n- Wait Until Element Is Visible form.buttons.save\n- Click Button Save\n+ Wait For Then Click Element form.buttons.save\n Wait until page contains Changes saved\n \n I enable filtering by workflow states\n Select Checkbox name=form.widgets.filter_on_workflow:list\n- Set Focus To Element form.buttons.save\n- Wait Until Element Is Visible form.buttons.save\n- Click Button Save\n+ Wait For Then Click Element form.buttons.save\n Wait until page contains Changes saved\n \n I choose to show \'${workflow_state}\' items\n Select Checkbox xpath=//input[@value=\'${workflow_state}\']\n- Set Focus To Element form.buttons.save\n- Wait Until Element Is Visible form.buttons.save\n- Click Button Save\n+ Wait For Then Click Element form.buttons.save\n Wait until page contains Changes saved\n \n I choose to not show \'${workflow_state}\' items\ndiff --git a/Products/CMFPlone/tests/robot/test_controlpanel_search.robot b/Products/CMFPlone/tests/robot/test_controlpanel_search.robot\nindex 395c160b30..de9c3b5cfe 100644\n--- a/Products/CMFPlone/tests/robot/test_controlpanel_search.robot\n+++ b/Products/CMFPlone/tests/robot/test_controlpanel_search.robot\n@@ -49,18 +49,15 @@ the search control panel\n \n I enable livesearch\n Select Checkbox form.widgets.enable_livesearch:list\n- Set Focus To Element css=#form-buttons-save\n- Wait Until Element Is Visible css=#form-buttons-save\n- Click Button Save\n+ Wait For Then Click Element css=#form-buttons-save\n Wait until page contains Changes saved\n \n I exclude the \'${portal_type}\' type from search\n # Make sure we see the checkbox, in expanded in jenkins it gets a bit under the toolbar\n- Set Focus To Element xpath=//input[@name=\'form.widgets.types_not_searched:list\' and @value=\'${portal_type}\']\n- Unselect Checkbox xpath=//input[@name=\'form.widgets.types_not_searched:list\' and @value=\'${portal_type}\']\n- Set Focus To Element css=#form-buttons-save\n- Wait Until Element Is Visible css=#form-buttons-save\n- Click Button Save\n+ ${element} Set Variable xpath=//input[@name=\'form.widgets.types_not_searched:list\' and @value=\'${portal_type}\']\n+ Wait For Element ${element}\n+ Unselect Checkbox ${element}\n+ Wait For Then Click Element css=#form-buttons-save\n Wait until page contains Changes saved\n \n \ndiff --git a/Products/CMFPlone/tests/robot/test_controlpanel_security.robot b/Products/CMFPlone/tests/robot/test_controlpanel_security.robot\nindex 4b7066cb31..535f96945e 100644\n--- a/Products/CMFPlone/tests/robot/test_controlpanel_security.robot\n+++ b/Products/CMFPlone/tests/robot/test_controlpanel_security.robot\n@@ -64,9 +64,9 @@ the security control panel\n \n a published test folder\n Go to ${PLONE_URL}/test-folder\n- Wait until element is visible css=#plone-contentmenu-workflow\n+ Wait For Element css=#plone-contentmenu-workflow\n Click link xpath=//li[@id=\'plone-contentmenu-workflow\']/a\n- Wait until element is visible id=workflow-transition-publish\n+ Wait For Element id=workflow-transition-publish\n Click link id=workflow-transition-publish\n Wait until page contains Item state changed\n \n@@ -142,9 +142,7 @@ A user folder should be created when a user registers and logs in to the site\n Input Text for sure form.widgets.email joe@test.com\n Input Text for sure form.widgets.password supersecret\n Input Text for sure form.widgets.password_ctl supersecret\n- Set Focus To Element css=#form-buttons-register\n- Wait Until Element Is Visible css=#form-buttons-register\n- Click Button Register\n+ Wait For Then Click Element css=#form-buttons-register\n \n # I login to the site\n Go to ${PLONE_URL}/login\n@@ -176,9 +174,7 @@ UUID should be used for the user id\n Input Text for sure form.widgets.email joe@test.com\n Input Text for sure form.widgets.password supersecret\n Input Text for sure form.widgets.password_ctl supersecret\n- Set Focus To Element css=#form-buttons-register\n- Wait Until Element Is Visible css=#form-buttons-register\n- Click Button Register\n+ Wait For Then Click Element css=#form-buttons-register\n \n # I login to the site\n Go to ${PLONE_URL}/login\ndiff --git a/Products/CMFPlone/tests/robot/test_controlpanel_site.robot b/Products/CMFPlone/tests/robot/test_controlpanel_site.robot\nindex 81fee76ee0..1844ac2d65 100644\n--- a/Products/CMFPlone/tests/robot/test_controlpanel_site.robot\n+++ b/Products/CMFPlone/tests/robot/test_controlpanel_site.robot\n@@ -63,44 +63,33 @@ the site control panel\n \n I enable the sitemap\n Given patterns are loaded\n- Set Focus To Element css=#formfield-form-widgets-enable_sitemap\n- Wait Until Element Is Visible css=#formfield-form-widgets-enable_sitemap\n+ Wait For Element css=#formfield-form-widgets-enable_sitemap\n Select Checkbox form.widgets.enable_sitemap:list\n- Set Focus To Element css=#form-buttons-save\n- Wait Until Element Is Visible css=#form-buttons-save\n- Click Button Save\n+ Wait For Then Click Element css=#form-buttons-save\n Wait until page contains Changes saved\n \n I set the site title to \'${site_title}\'\n Given patterns are loaded\n Input Text name=form.widgets.site_title ${site_title}\n- Set Focus To Element css=#form-buttons-save\n- Wait Until Element Is Visible css=#form-buttons-save\n- Click Button Save\n+ Wait For Then Click Element css=#form-buttons-save\n Wait until page contains Changes saved\n \n I set a custom logo\n Given patterns are loaded\n Choose File name=form.widgets.site_logo ${PATH_TO_TEST_FILES}/pixel.png\n- Set Focus To Element css=#form-buttons-save\n- Wait Until Element Is Visible css=#form-buttons-save\n- Click Button Save\n+ Wait For Then Click Element css=#form-buttons-save\n Wait until page contains Changes saved\n \n I enable dublin core metadata\n Given patterns are loaded\n Select Checkbox form.widgets.exposeDCMetaTags:list\n- Set Focus To Element css=#form-buttons-save\n- Wait Until Element Is Visible css=#form-buttons-save\n- Click Button Save\n+ Wait For Then Click Element css=#form-buttons-save\n Wait until page contains Changes saved\n \n I add a Javascript snippet to the webstats javascript\n Given patterns are loaded\n Input Text name=form.widgets.webstats_js \n- Set Focus To Element css=#form-buttons-save\n- Wait Until Element Is Visible css=#form-buttons-save\n- Click Button Save\n+ Wait For Then Click Element css=#form-buttons-save\n Wait until page contains Changes saved\n \n \ndiff --git a/Products/CMFPlone/tests/robot/test_controlpanel_social.robot b/Products/CMFPlone/tests/robot/test_controlpanel_social.robot\nindex 70f106d2e0..2fae936771 100644\n--- a/Products/CMFPlone/tests/robot/test_controlpanel_social.robot\n+++ b/Products/CMFPlone/tests/robot/test_controlpanel_social.robot\n@@ -44,20 +44,14 @@ the social control panel\n \n I disable social\n UnSelect Checkbox form.widgets.share_social_data:list\n- Sleep 2\n- Set Focus To Element css=#form-buttons-save\n- Wait Until Element Is Visible css=#form-buttons-save\n- Click Button Save\n+ Wait For Then Click Element css=#form-buttons-save\n Wait until page contains Changes saved\n \n I provide social settings\n Input Text name=form.widgets.twitter_username plonecms\n Input Text name=form.widgets.facebook_app_id 123456\n Input Text name=form.widgets.facebook_username plonecms\n- Sleep 2\n- Set Focus To Element css=#form-buttons-save\n- Wait Until Element Is Visible css=#form-buttons-save\n- Click Button Save\n+ Wait For Then Click Element css=#form-buttons-save\n Wait until page contains Changes saved\n \n \ndiff --git a/Products/CMFPlone/tests/robot/test_edit_user_schema.robot b/Products/CMFPlone/tests/robot/test_edit_user_schema.robot\nindex bbb8e87b4f..439bb5b074 100644\n--- a/Products/CMFPlone/tests/robot/test_edit_user_schema.robot\n+++ b/Products/CMFPlone/tests/robot/test_edit_user_schema.robot\n@@ -96,16 +96,15 @@ I add a new text field to the member fields\n Click Link Add new field\xe2\x80\xa6\n Wait Until Element Is visible css=#add-field-form #form-widgets-title\n Input Text css=#add-field-form #form-widgets-title Test Field\n- Press Key css=#add-field-form #form-widgets-title \\\\09\n+ Press Keys css=#add-field-form #form-widgets-title TAB\n Select From List By Label css=#form-widgets-factory Text line (String)\n Click button css=.pattern-modal-buttons button#form-buttons-add\n Wait until page contains Field added successfully.\n \n I Open the test_field Settings\n Go to ${PLONE_URL}/@@member-fields\n- Wait until page contains element css=div[data-field_id=\'test_field\']\n- Set Focus To Element css=div[data-field_id=\'test_field\'] a.fieldSettings\n- Wait Until Keyword Succeeds 3 100ms Click link css=div[data-field_id=\'test_field\'] a.fieldSettings\n+ Wait For Element css=div[data-field_id=\'test_field\']\n+ Wait For Then Click Element css=div[data-field_id=\'test_field\'] a.fieldSettings\n \n I add a new required text field to the member fields\n Go to ${PLONE_URL}/@@member-fields\n@@ -113,7 +112,7 @@ I add a new required text field to the member fields\n Click Link Add new field\xe2\x80\xa6\n Wait Until Element Is visible css=#add-field-form #form-widgets-title\n Input Text css=#add-field-form #form-widgets-title Test Field\n- Press Key css=#add-field-form #form-widgets-title \\\\09\n+ Press Keys css=#add-field-form #form-widgets-title TAB\n Select From List By Label css=#form-widgets-factory Text line (String)\n Select Checkbox form.widgets.required:list\n Click button css=.pattern-modal-buttons button#form-buttons-add\n@@ -142,6 +141,7 @@ add a min/max constraint to the field\n Wait until page contains element form.widgets.min_length\n Input Text form.widgets.min_length 4\n Input Text form.widgets.max_length 6\n+ Wait Until Element Is visible css=.pattern-modal-buttons button#form-buttons-save\n Click Button css=.pattern-modal-buttons button#form-buttons-save\n Sleep 1\n \n@@ -172,7 +172,8 @@ a logged-in user will see the field on top of the user profile\n a logged-in user will see a field with min/max constraints\n a logged-in user will see the field in the user profile\n Input Text form.widgets.email test@plone.org\n+ Wait For Element css=#form-widgets-test_field\n Input Text form.widgets.test_field 1\n- Click Button Save\n+ Wait For Then Click Element css=.formControls button#form-buttons-save\n Wait until page contains There were some errors.\n Page should contain Value is too short\ndiff --git a/Products/CMFPlone/tests/robot/test_linkintegrity.robot b/Products/CMFPlone/tests/robot/test_linkintegrity.robot\nindex bf2b03adcf..db3f9d888f 100644\n--- a/Products/CMFPlone/tests/robot/test_linkintegrity.robot\n+++ b/Products/CMFPlone/tests/robot/test_linkintegrity.robot\n@@ -77,6 +77,8 @@ a link in rich text\n Click Button css=button[aria-label="Insert/edit link"]\n \n Given patterns are loaded\n+ # Somehow this does not work:\n+ # Wait For Then Click Element css=.pat-relateditems .select2-input.select2-default\n Wait until element is visible css=.pat-relateditems .select2-input.select2-default\n Click Element css=.pat-relateditems .select2-input.select2-default\n Wait until element is visible css=.pat-relateditems-result.one-level-up a.pat-relateditems-result-browse\n@@ -84,68 +86,59 @@ a link in rich text\n Wait until element is visible xpath=(//span[contains(., \'Foo\')])\n Click Element xpath=(//span[contains(., \'Foo\')])\n Wait until page contains Foo\n-\n- Click Button css=.modal-footer .btn-primary\n- Click Button css=#form-buttons-save\n+ Wait For Then Click Element css=.modal-footer .btn-primary\n+ Wait For Then Click Element css=#form-buttons-save\n \n \n should show warning when deleting page\n+\n Go To ${PLONE_URL}/foo\n- Wait until element is visible css=#plone-contentmenu-actions a\n- Click Link css=#plone-contentmenu-actions a\n- Wait until element is visible css=#plone-contentmenu-actions-delete\n- Click Link css=#plone-contentmenu-actions-delete\n+ Wait For Then Click Element css=#plone-contentmenu-actions a\n+ Wait For Then Click Element css=#plone-contentmenu-actions-delete\n Wait until page contains element css=.breach-container .breach-item\n \n \n should show warning when deleting page from folder_contents\n Go To ${PLONE_URL}/folder_contents\n- Wait until keyword succeeds 30 1 Page should contain element css=tr[data-id="foo"] input\n- # Might still take a bit before it is clickable.\n- Sleep 1\n- Click Element css=tr[data-id="foo"] input\n+ Wait For Then Click Element css=tr[data-id="foo"] input\n Checkbox Should Be Selected css=tr[data-id="foo"] input\n Wait until keyword succeeds 30 1 Page should not contain element css=#btn-delete.disabled\n- Click Element css=#btngroup-mainbuttons #btn-delete\n+\n+ Wait For Then Click Element css=#btngroup-mainbuttons #btn-delete\n Wait until page contains element css=.popover-content .btn-danger\n Page should contain element css=.breach-container .breach-item\n- Click Element css=#popover-delete .closeBtn\n+ Wait For Then Click Element css=#popover-delete .closeBtn\n Checkbox Should Be Selected css=tr[data-id="foo"] input\n \n \n should not show warning when deleting page from folder_contents\n Go To ${PLONE_URL}/folder_contents\n Wait until page contains element css=tr[data-id="foo"] input\n- # Might still take a bit before it is clickable.\n- Sleep 1\n- Click Element css=tr[data-id="foo"] input\n+ Wait For Then Click Element css=tr[data-id="foo"] input\n Checkbox Should Be Selected css=tr[data-id="foo"] input\n Wait until keyword succeeds 30 1 Page should not contain element css=#btn-delete.disabled\n- Click Element css=#btngroup-mainbuttons #btn-delete\n+ Wait For Then Click Element css=#btngroup-mainbuttons #btn-delete\n Wait until page contains element css=.popover-content .btn-danger\n Page should not contain element css=.breach-container .breach-item\n- Click Element css=#popover-delete .applyBtn\n+ Wait For Then Click Element css=#popover-delete .applyBtn\n Wait until page contains Successfully delete items\n Wait until keyword succeeds 30 1 Page should not contain Element css=tr[data-id="foo"] input\n \n \n should not show warning when deleting page\n Go To ${PLONE_URL}/foo\n- Wait until element is visible css=#plone-contentmenu-actions a\n- Click Link css=#plone-contentmenu-actions a\n- Wait until element is visible css=#plone-contentmenu-actions-delete\n- Click Link css=#plone-contentmenu-actions-delete\n+ Wait For Then Click Element css=#plone-contentmenu-actions a\n+ Wait For Then Click Element css=#plone-contentmenu-actions-delete\n Page should not contain element css=.breach-container .breach-item\n \n \n remove link to page\n Go To ${PLONE_URL}/bar\n- Wait until element is visible css=#contentview-edit a\n- Click Link css=#contentview-edit a\n- Wait until element is visible css=.tox-edit-area iframe\n+ Wait For Then Click Element css=#contentview-edit a\n+ Wait For Element css=.tox-edit-area iframe\n Select Frame css=.tox-edit-area iframe\n Input text css=.mce-content-body foo\n Execute Javascript function selectElementContents(el) {var range = document.createRange(); range.selectNodeContents(el); var sel = window.getSelection(); sel.removeAllRanges(); sel.addRange(range);} var el = document.getElementById("tinymce"); selectElementContents(el);\n UnSelect Frame\n Click Button css=button[aria-label="Remove link"]\n- Click Button css=#form-buttons-save\n+ Wait For Then Click Element css=#form-buttons-save\ndiff --git a/Products/CMFPlone/tests/robot/test_livesearch.robot b/Products/CMFPlone/tests/robot/test_livesearch.robot\nindex 0cbbb8280d..8bed725311 100644\n--- a/Products/CMFPlone/tests/robot/test_livesearch.robot\n+++ b/Products/CMFPlone/tests/robot/test_livesearch.robot\n@@ -55,13 +55,13 @@ a news item\n I search for\n [Arguments] ${searchtext}\n Input text css=input#searchGadget ${searchtext}\n- Set Focus To Element css=input#searchGadget\n+ Wait For Element css=input#searchGadget\n \n I search the currentfolder only for\n [Arguments] ${searchtext}\n Select checkbox id=searchbox_currentfolder_only\n Input text css=input#searchGadget ${searchtext}\n- Set Focus To Element css=input#searchGadget\n+ Wait For Element css=input#searchGadget\n \n the livesearch results should contain\n [Arguments] ${text}\ndiff --git a/Products/CMFPlone/tests/robot/test_querystring.robot b/Products/CMFPlone/tests/robot/test_querystring.robot\nindex ef22e81206..2ab1944c6a 100644\n--- a/Products/CMFPlone/tests/robot/test_querystring.robot\n+++ b/Products/CMFPlone/tests/robot/test_querystring.robot\n@@ -72,8 +72,7 @@ Scenario: Searchable text query\n When I open the criteria Searchable text\n and I search for a\n and Sleep 0.2\n- and Wait Until Element Is Visible css=div.querystring-preview\n- and Click Element css=div.querystring-preview\n+ and Wait For Then Click Element css=div.querystring-preview\n Then we expect 2 hits\n When I open the criteria Searchable text\n and I search for d\n@@ -86,21 +85,18 @@ Scenario: Tag query one\n and the querystring pattern\n When I activate the default operator in the criteria Tag\n and I open the Selection Widget\n- and Wait Until Element Is Visible css=li.select2-results-dept-0.select2-result.select2-result-selectable.select2-option-o\n- and Click Element css=li.select2-results-dept-0.select2-result.select2-result-selectable.select2-option-o\n+ ${base_option_selector} Set Variable li.select2-results-dept-0.select2-result.select2-result-selectable.select2-option\n+ and Wait For Then Click Element css=${base_option_selector}-o\n Then we expect 4 hits\n When I open the Selection Widget\n- and Wait Until Element Is Visible css=li.select2-results-dept-0.select2-result.select2-result-selectable.select2-option-n\n- and Click Element css=li.select2-results-dept-0.select2-result.select2-result-selectable.select2-option-n\n+ and Wait For Then Click Element css=${base_option_selector}-n\n Then we expect 4 hits\n When I delete my selection\n and I open the Selection Widget\n- and Wait Until Element Is Visible css=li.select2-results-dept-0.select2-result.select2-result-selectable.select2-option-p\n- and Click Element css=li.select2-results-dept-0.select2-result.select2-result-selectable.select2-option-p\n+ and Wait For Then Click Element css=${base_option_selector}-p\n Then we expect 1 hits\n When I open the Selection Widget\n- and Wait Until Element Is Visible css=li.select2-results-dept-0.select2-result.select2-result-selectable.select2-option-e\n- and Click Element css=li.select2-results-dept-0.select2-result.select2-result-selectable.select2-option-e\n+ and Wait For Then Click Element css=${base_option_selector}-e\n Then we expect 2 hits\n \n Scenario Tag query two\n@@ -110,21 +106,18 @@ Scenario Tag query two\n and the querystring pattern\n When I expect an empty result after open the operator Matches all of in the criteria Tag\n and I open the Selection Widget\n- and Wait Until Element Is Visible css=li.select2-results-dept-0.select2-result.select2-result-selectable.select2-option-o\n- and Click Element css=li.select2-results-dept-0.select2-result.select2-result-selectable.select2-option-o\n+ ${base_option_selector} Set Variable li.select2-results-dept-0.select2-result.select2-result-selectable.select2-option\n+ and Wait For Then Click Element css=${base_option_selector}-o\n Then we expect 4 hits\n When and I open the Selection Widget\n- and Wait Until Element Is Visible css=li.select2-results-dept-0.select2-result.select2-result-selectable.select2-option-n\n- and Click Element css=li.select2-results-dept-0.select2-result.select2-result-selectable.select2-option-n\n+ and Wait For Then Click Element css=${base_option_selector}-n\n Then we expect 3 hits\n When I delete my selection\n and and I open the Selection Widget\n- and Wait Until Element Is Visible css=li.select2-results-dept-0.select2-result.select2-result-selectable.select2-option-p\n- and Click Element css=li.select2-results-dept-0.select2-result.select2-result-selectable.select2-option-p\n+ and Wait For Then Click Element css=${base_option_selector}-p\n Then we expect 1 hits\n When and I open the Selection Widget\n- and Wait Until Element Is Visible css=li.select2-results-dept-0.select2-result.select2-result-selectable.select2-option-e\n- and Click Element css=li.select2-results-dept-0.select2-result.select2-result-selectable.select2-option-e\n+ and Wait For Then Click Element css=${base_option_selector}-e\n Then we expect 1 hits\n \n \n@@ -171,8 +164,7 @@ Scenario Review state query\n and the querystring pattern\n When I open the criteria Review State\n and I open the Selection Widget\n- and Wait Until Element Is Visible css=li.select2-option-private\n- and Click Element css=li.select2-option-private\n+ and Wait For Then Click Element css=li.select2-option-private\n Then we expect 7 hits\n \n Scenario Type query\n@@ -181,32 +173,26 @@ Scenario Type query\n and the querystring pattern\n When I open the criteria Type\n and I open the Selection Widget\n- and Wait Until Element Is Visible css=li.select2-option-event\n- and Click Element css=li.select2-option-event\n+ and Wait For Then Click Element css=li.select2-option-event\n Then we expect 4 hits\n When I delete one selection\n and I open the Selection Widget\n- and Wait Until Element Is Visible css=li.select2-option-file\n- and Click Element css=li.select2-option-file\n+ and Wait For Then Click Element css=li.select2-option-file\n Then we do not expect any hits\n When I delete one selection\n and I open the Selection Widget\n- and Wait Until Element Is Visible css=li.select2-option-folder\n- and Click Element css=li.select2-option-folder\n+ and Wait For Then Click Element css=li.select2-option-folder\n Then we expect 5 hits\n When I delete one selection\n and I open the Selection Widget\n- and Wait Until Element Is Visible css=li.select2-option-link\n- and Click Element css=li.select2-option-link\n+ and Wait For Then Click Element css=li.select2-option-link\n Then we expect 1 hits\n When I delete one selection\n and I open the Selection Widget\n- and Wait Until Element Is Visible css=li.select2-option-document\n- and Click Element css=li.select2-option-document\n+ and Wait For Then Click Element css=li.select2-option-document\n Then we expect 2 hits\n When I open the Selection Widget\n- and Wait Until Element Is Visible css=li.select2-option-link\n- and Click Element css=li.select2-option-link\n+ and Wait For Then Click Element css=li.select2-option-link\n Then we expect 3 hits\n \n Scenario Creator query\n@@ -264,7 +250,6 @@ I activate the operator ${OPERATOR} in the criteria ${CRITERIA}\n I expect an empty result after open the operator ${OPERATOR} in the criteria ${CRITERIA}\n open the select box titled index\n select index type ${CRITERIA}\n- sleep 0.75\n Wait for condition return $("dl.searchResults").length == 0\n open the select box titled operator\n select index type ${OPERATOR}\n@@ -274,59 +259,61 @@ I open the criteria ${CRITERIA}\n select index type ${CRITERIA}\n \n I search for ${KEYWORD}\n- Wait Until Element Is Visible css=input.querystring-criteria-value-StringWidget\n- Click Element css=input.querystring-criteria-value-StringWidget\n- Input Text css=input.querystring-criteria-value-StringWidget ${KEYWORD}\n+ ${keyword_selector} Set Variable input.querystring-criteria-value-StringWidget\n+ Wait For Then Click Element css=${keyword_selector}\n+ Input Text css=${keyword_selector} ${KEYWORD}\n Click Element css=div#content-core\n \n I open the Selection Widget\n- wait until element is visible css=div.select2-container-multi.querystring-criteria-value-MultipleSelectionWidget\n- click element css=div.select2-container-multi.querystring-criteria-value-MultipleSelectionWidget\n+ Wait For Then Click Element css=div.select2-container-multi.querystring-criteria-value-MultipleSelectionWidget\n \n I delete one selection\n #deletes one element\n- Click Element css=a.select2-search-choice-close\n+ Wait For Then Click Element css=a.select2-search-choice-close\n \n I delete my selection\n #deletes two elements\n- Click Element css=a.select2-search-choice-close\n- Click Element css=a.select2-search-choice-close\n+ Wait For Then Click Element css=a.select2-search-choice-close\n+ Sleep 0.1\n+ Wait For Then Click Element css=a.select2-search-choice-close\n \n I search in ${NAME} subfolder in the related items widget\n mark results\n- Click Element css=ul.select2-choices\n+ Wait For Then Click Element css=ul.select2-choices\n Wait Until Page Contains ${NAME}\n Click Element //a[contains(concat(\' \', normalize-space(@class), \' \'), \' pat-relateditems-result-select \')]//span[contains(text(),\'${NAME}\')]\n \n I expect to be in Advanced mode\n open the select box titled operator\n- Element Should Contain jquery=.select2-drop-active[style*="display: block;"] Navigation Path\n- Element Should Contain jquery=.select2-drop-active[style*="display: block;"] Absolute Path\n- Element Should Contain jquery=.select2-drop-active[style*="display: block;"] Relative Path\n- Element Should Contain jquery=.select2-drop-active[style*="display: block;"] Simple Mode\n- Click Element css=div#select2-drop-mask\n- Wait Until Element Is Not Visible css=div#select2-drop-mask\n+ ${selector} Set Variable .select2-drop-active[style*="display: block;"]\n+ Element Should Contain jquery=${selector} Navigation Path\n+ Element Should Contain jquery=${selector} Absolute Path\n+ Element Should Contain jquery=${selector} Relative Path\n+ Element Should Contain jquery=${selector} Simple Mode\n+ ${selector} Set Variable div#select2-drop-mask\n+ Wait For Then Click Invisible Element css=${selector}\n+ Wait Until Element Is Not Visible css=${selector}\n \n I expect to be in Simple mode\n open the select box titled operator\n- Element Should Contain jquery=.select2-drop-active[style*="display: block;"] Custom\n- Element Should Contain jquery=.select2-drop-active[style*="display: block;"] Parent (../)\n- Element Should Contain jquery=.select2-drop-active[style*="display: block;"] Current (./)\n- Element Should Contain jquery=.select2-drop-active[style*="display: block;"] Advanced Mode\n- Click Element css=div#select2-drop-mask\n- Wait Until Element Is Not Visible css=div#select2-drop-mask\n+ ${selector} Set Variable .select2-drop-active[style*="display: block;"]\n+ Element Should Contain jquery=${selector} Custom\n+ Element Should Contain jquery=${selector} Parent (../)\n+ Element Should Contain jquery=${selector} Current (./)\n+ Element Should Contain jquery=${selector} Advanced Mode\n+ ${selector} Set Variable div#select2-drop-mask\n+ Wait For Then Click Invisible Element css=${selector}\n+ Wait Until Element Is Not Visible css=${selector}\n \n open the select box titled ${NAME}\n Click Element css=body\n- ${select_criteria_selector} Set Variable .querystring-criteria-${NAME} .select2-container a\n- Wait Until Element Is Visible css=${select_criteria_selector}\n- Click Element css=${select_criteria_selector}\n+ Wait For Then Click Element css=.querystring-criteria-${NAME} .select2-container\n \n select index type ${INDEX}\n ${input_selector} Set Variable .select2-drop-active[style*="display: block;"] input\n- Wait Until Element Is Visible jquery=${input_selector}\n- Input Text jquery=${input_selector} text=${INDEX}\n- Press Key jquery=:focus \\\\13\n+ Wait For Element css=${input_selector}\n+ Input Text css=${input_selector} text=${INDEX}\n+ Press Keys jquery=:focus RETURN\n \n we expect ${NUM} hits\n #This assumes we have the 2 "Test document" and "Test folder" items from the\ndiff --git a/Products/CMFPlone/tests/robot/test_tinymce.robot b/Products/CMFPlone/tests/robot/test_tinymce.robot\nindex 8f56da5823..75eb8d00c7 100644\n--- a/Products/CMFPlone/tests/robot/test_tinymce.robot\n+++ b/Products/CMFPlone/tests/robot/test_tinymce.robot\n@@ -28,9 +28,7 @@ Scenario: A page is opened to edit in TinyMCE\n and insert link\n and insert image\n \n- Set Focus To Element css=#form-buttons-save\n- Wait Until Element Is Visible css=#form-buttons-save\n- Click Button Save\n+ Wait For Then Click Element css=#form-buttons-save\n Wait until page contains Changes saved\n \n \ndiff --git a/Products/CMFPlone/tests/testResourceRegistries.py b/Products/CMFPlone/tests/testResourceRegistries.py\nindex d7214266cf..e2113462aa 100644\n--- a/Products/CMFPlone/tests/testResourceRegistries.py\n+++ b/Products/CMFPlone/tests/testResourceRegistries.py\n@@ -76,9 +76,9 @@ def test_scripts_viewlet(self):\n scripts = ScriptsView(self.layer["portal"], self.layer["request"], None)\n scripts.update()\n results = scripts.render()\n- self.assertIn("++plone++static/bundle-jquery/jquery.min.js", results)\n+ self.assertIn("++plone++static/bundle-plone/jquery-remote.min.js", results)\n self.assertIn(\n- "++plone++static/bundle-bootstrap/js/bootstrap.bundle.min.js", results\n+ "++plone++static/bundle-plone/bootstrap-remote.min.js", results\n )\n self.assertIn("++plone++static/bundle-plone/bundle.min.js", results)\n self.assertIn("http://nohost/plone/++webresource++", results)\n@@ -88,9 +88,9 @@ def test_scripts_viewlet_anonymous(self):\n scripts = ScriptsView(self.layer["portal"], self.layer["request"], None)\n scripts.update()\n results = scripts.render()\n- self.assertIn("++plone++static/bundle-jquery/jquery.min.js", results)\n+ self.assertIn("++plone++static/bundle-plone/jquery-remote.min.js", results)\n self.assertIn(\n- "++plone++static/bundle-bootstrap/js/bootstrap.bundle.min.js", results\n+ "++plone++static/bundle-plone/bootstrap-remote.min.js", results\n )\n self.assertIn("++plone++static/bundle-plone/bundle.min.js", results)\n self.assertIn("http://nohost/plone/++webresource++", results)\ndiff --git a/news/3528.feature b/news/3528.feature\nnew file mode 100644\nindex 0000000000..6b0488bd07\n--- /dev/null\n+++ b/news/3528.feature\n@@ -0,0 +1,2 @@\n+Show link to the Volto-migration (@@migrate_to_volto) in the view @@plone-upgrade when the option is available.\n+[pbauer]\n\\ No newline at end of file\ndiff --git a/news/3539.bugfix b/news/3539.bugfix\nnew file mode 100644\nindex 0000000000..6a03bb0dca\n--- /dev/null\n+++ b/news/3539.bugfix\n@@ -0,0 +1,2 @@\n+Fix tests for updated module federation bundles.\n+[thet]\n' +b'diff --git a/plone/outputfilters/tests/test_resolveuid_and_caption.py b/plone/outputfilters/tests/test_resolveuid_and_caption.py\nindex f011457..411a74f 100644\n--- a/plone/outputfilters/tests/test_resolveuid_and_caption.py\n+++ b/plone/outputfilters/tests/test_resolveuid_and_caption.py\n@@ -344,7 +344,7 @@ def test_image_captioning_in_news_item(self):\n \n def test_image_captioning_absolutizes_uncaptioned_image(self):\n text_in = """"""\n- text_out = """"""\n+ text_out = """"""\n self._assertTransformsTo(text_in, text_out)\n \n def test_image_captioning_absolute_path(self):\n' -Repository: Products.CMFPlone +Repository: plone.outputfilters Branch: refs/heads/master -Date: 2022-06-09T22:32:39+02:00 +Date: 2022-06-08T17:57:33+03:00 +Author: MrTango (MrTango) +Commit: https://github.com/plone/plone.outputfilters/commit/52a4650feb943c53d128b8779b683ad281ddd706 + +use plone.app.uuid.utils.uuidToObject, add dependency to plone.namedfile + +Files changed: +M plone/outputfilters/filters/resolveuid_and_caption.py +M setup.py + +b"diff --git a/plone/outputfilters/filters/resolveuid_and_caption.py b/plone/outputfilters/filters/resolveuid_and_caption.py\nindex 014cd14..b48c07d 100644\n--- a/plone/outputfilters/filters/resolveuid_and_caption.py\n+++ b/plone/outputfilters/filters/resolveuid_and_caption.py\n@@ -6,7 +6,7 @@\n from bs4 import BeautifulSoup\n from DocumentTemplate.html_quote import html_quote\n from DocumentTemplate.DT_Var import newline_to_br\n-from plone.outputfilters.browser.resolveuid import uuidToObject\n+from plone.app.uuid.utils import uuidToObject\n from plone.outputfilters.interfaces import IFilter\n from plone.registry.interfaces import IRegistry\n from Products.CMFCore.interfaces import IContentish\ndiff --git a/setup.py b/setup.py\nindex 4ad8816..27d151d 100644\n--- a/setup.py\n+++ b/setup.py\n@@ -66,6 +66,7 @@ def read(filename):\n 'Products.GenericSetup',\n 'Products.MimetypesRegistry',\n 'Products.PortalTransforms>=2.0',\n+ 'plone.namedfile',\n 'setuptools',\n 'six',\n 'unidecode',\n" + +Repository: plone.outputfilters + + +Branch: refs/heads/master +Date: 2022-06-08T17:02:36+02:00 Author: Maurits van Rees (mauritsvanrees) -Commit: https://github.com/plone/Products.CMFPlone/commit/40be9133dd26ea159e52320c966616710b61394f +Commit: https://github.com/plone/plone.outputfilters/commit/e434a29906819efa01d08865fbf54b026fec9e42 + +Add dependency on plone.app.uuid + +Files changed: +M setup.py -Restore imageScales / image_scales in patterns settings. +b"diff --git a/setup.py b/setup.py\nindex 27d151d..8cecaca 100644\n--- a/setup.py\n+++ b/setup.py\n@@ -67,6 +67,7 @@ def read(filename):\n 'Products.MimetypesRegistry',\n 'Products.PortalTransforms>=2.0',\n 'plone.namedfile',\n+ 'plone.app.uuid',\n 'setuptools',\n 'six',\n 'unidecode',\n" -Otherwise the new code with picture variants is only used when you have the right mockup branch, -and mockup running, and the resource registries setup to use it, -or the mockup changes should have landed in plone.staticresources already. +Repository: plone.outputfilters -Note that the breakage is not too bad though: -if imageScales is not in the patterns settings and you still have TinyMCE from old staticresources, -then TinyMCE will still work. It will just use an internal copy of the imageScales, -which for example is missing the new greater and huge scales. -With the current commit, we support both old and new TinyMCE, as long as we think this is needed. + +Branch: refs/heads/master +Date: 2022-06-08T18:13:09+03:00 +Author: MrTango (MrTango) +Commit: https://github.com/plone/plone.outputfilters/commit/bfd44edf233e2bbaa40f1cb62537f0bb22090151 + +deactivate print statement + +Files changed: +M plone/outputfilters/tests/test_picture_variants.py +M plone/outputfilters/tests/test_resolveuid_and_caption.py + +b'diff --git a/plone/outputfilters/tests/test_picture_variants.py b/plone/outputfilters/tests/test_picture_variants.py\nindex f5fc0fa..542c161 100644\n--- a/plone/outputfilters/tests/test_picture_variants.py\n+++ b/plone/outputfilters/tests/test_picture_variants.py\n@@ -78,8 +78,8 @@ def _assertTransformsTo(self, input, expected):\n out = self.parser(input)\n normalized_out = normalize_html(out)\n normalized_expected = normalize_html(expected)\n- print("\\n e: {}".format(expected))\n- print("\\n o: {}".format(out))\n+ # print("\\n e: {}".format(expected))\n+ # print("\\n o: {}".format(out))\n try:\n self.assertTrue(_ellipsis_match(normalized_expected,\n normalized_out))\ndiff --git a/plone/outputfilters/tests/test_resolveuid_and_caption.py b/plone/outputfilters/tests/test_resolveuid_and_caption.py\nindex 411a74f..b250951 100644\n--- a/plone/outputfilters/tests/test_resolveuid_and_caption.py\n+++ b/plone/outputfilters/tests/test_resolveuid_and_caption.py\n@@ -81,8 +81,8 @@ def _assertTransformsTo(self, input, expected, parsing=True):\n out = input\n normalized_out = normalize_html(out)\n normalized_expected = normalize_html(expected)\n- print("e: {}".format(normalized_expected))\n- print("o: {}".format(normalized_out))\n+ # print("e: {}".format(normalized_expected))\n+ # print("o: {}".format(normalized_out))\n try:\n self.assertTrue(_ellipsis_match(normalized_expected,\n normalized_out))\n' + +Repository: plone.outputfilters + + +Branch: refs/heads/master +Date: 2022-06-08T18:13:19+03:00 +Author: MrTango (MrTango) +Commit: https://github.com/plone/plone.outputfilters/commit/7b4a8f5bcb80936d7d122912c6753b97529446d4 + +Merge branch 'mrtango-image-sourcesets-filter' of https://github.com/plone/plone.outputfilters into mrtango-image-sourcesets-filter Files changed: -M Products/CMFPlone/patterns/settings.py +M setup.py -b'diff --git a/Products/CMFPlone/patterns/settings.py b/Products/CMFPlone/patterns/settings.py\nindex 7f1fb5979e..a8fe05d5a2 100644\n--- a/Products/CMFPlone/patterns/settings.py\n+++ b/Products/CMFPlone/patterns/settings.py\n@@ -71,6 +71,16 @@ def mark_special_links(self):\n }\n return result\n \n+ @property\n+ def image_scales(self):\n+ # Keep image_scales at least until https://github.com/plone/mockup/pull/1156\n+ # is merged and plone.staticresources is updated.\n+ factory = getUtility(IVocabularyFactory, "plone.app.vocabularies.ImagesScales")\n+ vocabulary = factory(self.context)\n+ ret = [{"title": translate(it.title), "value": it.value} for it in vocabulary]\n+ ret = sorted(ret, key=lambda it: it["title"])\n+ return json.dumps(ret)\n+\n @property\n def picture_variants(self):\n registry = getUtility(IRegistry)\n@@ -140,6 +150,9 @@ def tinymce(self):\n configuration = {\n "base_url": self.context.absolute_url(),\n "imageTypes": image_types,\n+ # Keep imageScales at least until https://github.com/plone/mockup/pull/1156\n+ # is merged and plone.staticresources is updated.\n+ "imageScales": self.image_scales,\n "pictureVariants": self.picture_variants,\n "imageCaptioningEnabled": self.image_captioning,\n "linkAttribute": "UID",\n' +b"diff --git a/setup.py b/setup.py\nindex 27d151d..8cecaca 100644\n--- a/setup.py\n+++ b/setup.py\n@@ -67,6 +67,7 @@ def read(filename):\n 'Products.MimetypesRegistry',\n 'Products.PortalTransforms>=2.0',\n 'plone.namedfile',\n+ 'plone.app.uuid',\n 'setuptools',\n 'six',\n 'unidecode',\n" -Repository: Products.CMFPlone +Repository: plone.outputfilters Branch: refs/heads/master -Date: 2022-06-09T22:40:24+02:00 +Date: 2022-06-08T17:35:28+02:00 Author: Maurits van Rees (mauritsvanrees) -Commit: https://github.com/plone/Products.CMFPlone/commit/a1c4159b1a2e1695c2005d32fcb39a8becc59809 +Commit: https://github.com/plone/plone.outputfilters/commit/6ee71503c52c422a795fba74625b46f3f6a553df + +Add some deprecation warnings in resolveuid. -Merge branch 'master' into mrtango-image-handling-sourcesets-settings +Other code should import from plone.app.uuid.utils. +Removed our tests for these functions: uuidToObject and uuidToURL. Files changed: -A news/3555.bugfix -M Products/CMFPlone/profiles/default/actions.xml -M Products/CMFPlone/tests/robot/test_querystring.robot +M plone/outputfilters/browser/resolveuid.py +M plone/outputfilters/tests/test_resolveuid_and_caption.py +M setup.py -b'diff --git a/Products/CMFPlone/profiles/default/actions.xml b/Products/CMFPlone/profiles/default/actions.xml\nindex 18f0a826bf..0f79034d3f 100644\n--- a/Products/CMFPlone/profiles/default/actions.xml\n+++ b/Products/CMFPlone/profiles/default/actions.xml\n@@ -273,7 +273,7 @@\n \n \n True\n- {"title": "Log in", "width": "26em", "actionOptions": {"redirectOnResponse": true}}\n+ {}\n \n \n Register\n@@ -285,7 +285,7 @@\n \n \n True\n- {"prependContent": ".portalMessage"}\n+ {}\n \n \n Undo\ndiff --git a/Products/CMFPlone/tests/robot/test_querystring.robot b/Products/CMFPlone/tests/robot/test_querystring.robot\nindex 2ab1944c6a..989187671c 100644\n--- a/Products/CMFPlone/tests/robot/test_querystring.robot\n+++ b/Products/CMFPlone/tests/robot/test_querystring.robot\n@@ -213,6 +213,8 @@ the querystring pattern\n Go to ${PLONE_URL}/a/++add++Collection\n Wait until page contains element css=.pat-querystring\n Given querystring pattern loaded\n+ # Set a title, otherwise you see \'Please fill out this field\'\n+ Execute Javascript $(\'#form-widgets-IDublinCore-title\').val(\'A Collection\'); return 0;\n # for some unknown reason unload protection pops up, but only in robot tests\n Execute Javascript $(window).unbind(\'beforeunload\')\n \ndiff --git a/news/3555.bugfix b/news/3555.bugfix\nnew file mode 100644\nindex 0000000000..b5a978dc3a\n--- /dev/null\n+++ b/news/3555.bugfix\n@@ -0,0 +1,2 @@\n+Remove modal from login and join action.\n+[agitator]\n\\ No newline at end of file\n' +b'diff --git a/plone/outputfilters/browser/resolveuid.py b/plone/outputfilters/browser/resolveuid.py\nindex e774de2..0022053 100644\n--- a/plone/outputfilters/browser/resolveuid.py\n+++ b/plone/outputfilters/browser/resolveuid.py\n@@ -1,47 +1,33 @@\n # -*- coding: utf-8 -*-\n from Acquisition import aq_base\n+from plone.app.uuid.utils import uuidToURL\n+from plone.uuid.interfaces import IUUID\n from Products.CMFCore.utils import getToolByName\n from zExceptions import NotFound\n+from zope.component.hooks import getSite\n+from zope.deprecation import deprecate\n from zope.interface import implementer\n from zope.publisher.browser import BrowserView\n from zope.publisher.interfaces import IPublishTraverse\n \n+import zope.deferredimport\n \n-try:\n- from zope.component.hooks import getSite\n-except ImportError:\n- from zope.app.component.hooks import getSite\n \n-\n-def uuidToURL(uuid):\n- """Resolves a UUID to a URL via the UID index of portal_catalog.\n- """\n- catalog = getToolByName(getSite(), \'portal_catalog\')\n- res = catalog.unrestrictedSearchResults(UID=uuid)\n- if res:\n- return res[0].getURL()\n-\n-\n-def uuidToObject(uuid):\n- """Resolves a UUID to an object via the UID index of portal_catalog.\n- """\n- catalog = getToolByName(getSite(), \'portal_catalog\')\n- res = catalog.unrestrictedSearchResults(UID=uuid)\n- if res:\n- return res[0]._unrestrictedGetObject()\n+zope.deferredimport.initialize()\n+zope.deferredimport.deprecated(\n+ "Import from plone.app.uuid.utils instead",\n+ uuidToObject=\'plone.app.uuid:utils.uuidToObject\',\n+ # This does not seem to work, since we need it ourselves in this file:\n+ # uuidToURL=\'plone.app.uuid:utils.uuidToURL\',\n+)\n \n \n-try:\n- from plone.uuid.interfaces import IUUID\n-except ImportError:\n- def uuidFor(obj):\n- return obj.UID()\n-else:\n- def uuidFor(obj):\n- uuid = IUUID(obj, None)\n- if uuid is None and hasattr(aq_base(obj), \'UID\'):\n- uuid = obj.UID()\n- return uuid\n+@deprecate(\'uuidFor is no longer used and supported, will be removed in Plone 7.\')\n+def uuidFor(obj):\n+ uuid = IUUID(obj, None)\n+ if uuid is None and hasattr(aq_base(obj), \'UID\'):\n+ uuid = obj.UID()\n+ return uuid\n \n \n @implementer(IPublishTraverse)\ndiff --git a/plone/outputfilters/tests/test_resolveuid_and_caption.py b/plone/outputfilters/tests/test_resolveuid_and_caption.py\nindex b250951..2d2c344 100644\n--- a/plone/outputfilters/tests/test_resolveuid_and_caption.py\n+++ b/plone/outputfilters/tests/test_resolveuid_and_caption.py\n@@ -303,27 +303,6 @@ def test_resolveuid_view_querystring(self):\n self.assertEqual(\'http://nohost/plone/image.jpg?qs\',\n res.headers[\'location\'])\n \n- def test_uuidToURL(self):\n- from plone.outputfilters.browser.resolveuid import uuidToURL\n- self.assertEqual(\'http://nohost/plone/image.jpg\',\n- uuidToURL(self.UID))\n-\n- def test_uuidToObject(self):\n- from plone.outputfilters.browser.resolveuid import uuidToObject\n- self.assertTrue(self.portal[\'image.jpg\'].aq_base\n- is uuidToObject(self.UID).aq_base)\n-\n- def test_uuidToURL_permission(self):\n- from plone.outputfilters.browser.resolveuid import uuidToURL\n- from plone.outputfilters.browser.resolveuid import uuidToObject\n- self.portal.invokeFactory(\'Document\', id=\'page\', title=\'Page\')\n- page = self.portal[\'page\']\n- self.logout()\n- self.assertEqual(\'http://nohost/plone/page\',\n- uuidToURL(page.UID()))\n- self.assertTrue(page.aq_base\n- is uuidToObject(page.UID()).aq_base)\n-\n def test_image_captioning_in_news_item(self):\n # Create a news item with a relative unscaled image\n self.portal.invokeFactory(\'News Item\', id=\'a-news-item\', title=\'Title\')\ndiff --git a/setup.py b/setup.py\nindex 8cecaca..17047da 100644\n--- a/setup.py\n+++ b/setup.py\n@@ -73,6 +73,8 @@ def read(filename):\n \'unidecode\',\n \'beautifulsoup4\',\n \'lxml\',\n+ \'zope.deferredimport\',\n+ \'zope.deprecation\',\n ],\n extras_require={\n \'test\': [\n' -Repository: Products.CMFPlone +Repository: plone.outputfilters Branch: refs/heads/master -Date: 2022-06-10T12:23:40+02:00 +Date: 2022-06-10T01:18:12+02:00 Author: Maurits van Rees (mauritsvanrees) -Commit: https://github.com/plone/Products.CMFPlone/commit/d416cd1fda2f51f298127a3f9f4cb29b742472ca +Commit: https://github.com/plone/plone.outputfilters/commit/5f74d9bdee2329e2279dd108f68f7369566df11f -Fix white space problem in browser_collection_views.txt. +Revert "Add some deprecation warnings in resolveuid." -With newer plone.outputfilters the spacing is slightly different. +This reverts commit 6ee71503c52c422a795fba74625b46f3f6a553df. +It causes test failures in plone.restapi. + +Also, we should test what happens in our own code when you are anonymous and we call `uuidToObject` on a uuid of a private object. +Actually, that seems to work, at least for a simple link, both with our version of this function and with the version from `plone.app.uuid`. +But I don't know why it works. Files changed: -M Products/CMFPlone/tests/browser_collection_views.txt +M plone/outputfilters/browser/resolveuid.py +M plone/outputfilters/tests/test_resolveuid_and_caption.py +M setup.py -b'diff --git a/Products/CMFPlone/tests/browser_collection_views.txt b/Products/CMFPlone/tests/browser_collection_views.txt\nindex 1410ef8387..c2458703d0 100644\n--- a/Products/CMFPlone/tests/browser_collection_views.txt\n+++ b/Products/CMFPlone/tests/browser_collection_views.txt\n@@ -48,6 +48,12 @@ Now let\'s login and visit the collection in the test browser:\n >>> browser.getControl(\'Log in\').click()\n >>> browser.open(\'http://nohost/plone/folder/collection\')\n \n+When checking if the collection text is in the output, we are not interested in differences in whitespace.\n+So we use a normalize function:\n+\n+ >>> def normalize(value):\n+ ... return value.translate(str.maketrans({" ": None, "\\n": None, "\\t": None, "\\r": None}))\n+\n Lets check the listing_view (Standard view):\n \n >>> browser.getLink(\'Standard view\').click()\n@@ -55,7 +61,7 @@ Lets check the listing_view (Standard view):\n True\n >>> collection_description in browser.contents\n True\n- >>> collection_text in browser.contents\n+ >>> normalize(collection_text) in normalize(browser.contents)\n True\n \n Lets check the summary_view (Summary view):\n@@ -65,7 +71,7 @@ Lets check the summary_view (Summary view):\n True\n >>> collection_description in browser.contents\n True\n- >>> collection_text in browser.contents\n+ >>> normalize(collection_text) in normalize(browser.contents)\n True\n \n Lets check the full_view (All content):\n@@ -75,7 +81,7 @@ Lets check the full_view (All content):\n True\n >>> collection_description in browser.contents\n True\n- >>> collection_text in browser.contents\n+ >>> normalize(collection_text) in normalize(browser.contents)\n True\n \n Lets check the tabular_view (Tabular view):\n@@ -86,7 +92,7 @@ Lets check the tabular_view (Tabular view):\n True\n >>> collection_description in browser.contents\n True\n- >>> collection_text in browser.contents\n+ >>> normalize(collection_text) in normalize(browser.contents)\n True\n \n Lets ensure that the text field is not requested on a folder. We\n@@ -110,5 +116,5 @@ Lets ensure text is shown when Collection is default view of a folder\n True\n >>> collection_description in browser.contents\n True\n- >>> collection_text in browser.contents\n+ >>> normalize(collection_text) in normalize(browser.contents)\n True\n' +b'diff --git a/plone/outputfilters/browser/resolveuid.py b/plone/outputfilters/browser/resolveuid.py\nindex 0022053..e774de2 100644\n--- a/plone/outputfilters/browser/resolveuid.py\n+++ b/plone/outputfilters/browser/resolveuid.py\n@@ -1,33 +1,47 @@\n # -*- coding: utf-8 -*-\n from Acquisition import aq_base\n-from plone.app.uuid.utils import uuidToURL\n-from plone.uuid.interfaces import IUUID\n from Products.CMFCore.utils import getToolByName\n from zExceptions import NotFound\n-from zope.component.hooks import getSite\n-from zope.deprecation import deprecate\n from zope.interface import implementer\n from zope.publisher.browser import BrowserView\n from zope.publisher.interfaces import IPublishTraverse\n \n-import zope.deferredimport\n \n+try:\n+ from zope.component.hooks import getSite\n+except ImportError:\n+ from zope.app.component.hooks import getSite\n \n-zope.deferredimport.initialize()\n-zope.deferredimport.deprecated(\n- "Import from plone.app.uuid.utils instead",\n- uuidToObject=\'plone.app.uuid:utils.uuidToObject\',\n- # This does not seem to work, since we need it ourselves in this file:\n- # uuidToURL=\'plone.app.uuid:utils.uuidToURL\',\n-)\n+\n+def uuidToURL(uuid):\n+ """Resolves a UUID to a URL via the UID index of portal_catalog.\n+ """\n+ catalog = getToolByName(getSite(), \'portal_catalog\')\n+ res = catalog.unrestrictedSearchResults(UID=uuid)\n+ if res:\n+ return res[0].getURL()\n+\n+\n+def uuidToObject(uuid):\n+ """Resolves a UUID to an object via the UID index of portal_catalog.\n+ """\n+ catalog = getToolByName(getSite(), \'portal_catalog\')\n+ res = catalog.unrestrictedSearchResults(UID=uuid)\n+ if res:\n+ return res[0]._unrestrictedGetObject()\n \n \n-@deprecate(\'uuidFor is no longer used and supported, will be removed in Plone 7.\')\n-def uuidFor(obj):\n- uuid = IUUID(obj, None)\n- if uuid is None and hasattr(aq_base(obj), \'UID\'):\n- uuid = obj.UID()\n- return uuid\n+try:\n+ from plone.uuid.interfaces import IUUID\n+except ImportError:\n+ def uuidFor(obj):\n+ return obj.UID()\n+else:\n+ def uuidFor(obj):\n+ uuid = IUUID(obj, None)\n+ if uuid is None and hasattr(aq_base(obj), \'UID\'):\n+ uuid = obj.UID()\n+ return uuid\n \n \n @implementer(IPublishTraverse)\ndiff --git a/plone/outputfilters/tests/test_resolveuid_and_caption.py b/plone/outputfilters/tests/test_resolveuid_and_caption.py\nindex 2d2c344..b250951 100644\n--- a/plone/outputfilters/tests/test_resolveuid_and_caption.py\n+++ b/plone/outputfilters/tests/test_resolveuid_and_caption.py\n@@ -303,6 +303,27 @@ def test_resolveuid_view_querystring(self):\n self.assertEqual(\'http://nohost/plone/image.jpg?qs\',\n res.headers[\'location\'])\n \n+ def test_uuidToURL(self):\n+ from plone.outputfilters.browser.resolveuid import uuidToURL\n+ self.assertEqual(\'http://nohost/plone/image.jpg\',\n+ uuidToURL(self.UID))\n+\n+ def test_uuidToObject(self):\n+ from plone.outputfilters.browser.resolveuid import uuidToObject\n+ self.assertTrue(self.portal[\'image.jpg\'].aq_base\n+ is uuidToObject(self.UID).aq_base)\n+\n+ def test_uuidToURL_permission(self):\n+ from plone.outputfilters.browser.resolveuid import uuidToURL\n+ from plone.outputfilters.browser.resolveuid import uuidToObject\n+ self.portal.invokeFactory(\'Document\', id=\'page\', title=\'Page\')\n+ page = self.portal[\'page\']\n+ self.logout()\n+ self.assertEqual(\'http://nohost/plone/page\',\n+ uuidToURL(page.UID()))\n+ self.assertTrue(page.aq_base\n+ is uuidToObject(page.UID()).aq_base)\n+\n def test_image_captioning_in_news_item(self):\n # Create a news item with a relative unscaled image\n self.portal.invokeFactory(\'News Item\', id=\'a-news-item\', title=\'Title\')\ndiff --git a/setup.py b/setup.py\nindex 17047da..8cecaca 100644\n--- a/setup.py\n+++ b/setup.py\n@@ -73,8 +73,6 @@ def read(filename):\n \'unidecode\',\n \'beautifulsoup4\',\n \'lxml\',\n- \'zope.deferredimport\',\n- \'zope.deprecation\',\n ],\n extras_require={\n \'test\': [\n' -Repository: Products.CMFPlone +Repository: plone.outputfilters Branch: refs/heads/master -Date: 2022-06-13T12:00:30+02:00 +Date: 2022-06-10T01:25:22+02:00 Author: Maurits van Rees (mauritsvanrees) -Commit: https://github.com/plone/Products.CMFPlone/commit/8b566c5acc713f1a5df0457270499b0e75dd3eb3 +Commit: https://github.com/plone/plone.outputfilters/commit/99f0203c62f5755bfa240e9e7e0c3958b0a85c75 -Merge branch 'master' into mrtango-image-handling-sourcesets-settings +Do add deprecation warnings for our own uuidToObject and uuidToURL. Files changed: -A Products/CMFPlone/browser/static/plone-logo.svg -A news/3558.feature -M Products/CMFPlone/browser/configure.zcml -M Products/CMFPlone/browser/templates/plone-addsite.pt -M Products/CMFPlone/browser/templates/plone-admin-logged-out.pt -M Products/CMFPlone/browser/templates/plone-overview.pt -M Products/CMFPlone/browser/templates/plone-upgrade.pt -M Products/CMFPlone/tests/test_utils.py -M Products/CMFPlone/utils.py +M plone/outputfilters/browser/resolveuid.py +M setup.py -b'diff --git a/Products/CMFPlone/browser/configure.zcml b/Products/CMFPlone/browser/configure.zcml\nindex 009ec8f691..419bbca7bc 100644\n--- a/Products/CMFPlone/browser/configure.zcml\n+++ b/Products/CMFPlone/browser/configure.zcml\n@@ -21,8 +21,8 @@\n title="Allow sendto" />\n \n \n \n \n+\n+\n+\ndiff --git a/Products/CMFPlone/browser/templates/plone-addsite.pt b/Products/CMFPlone/browser/templates/plone-addsite.pt\nindex 7dff6a74c5..95641ab424 100644\n--- a/Products/CMFPlone/browser/templates/plone-addsite.pt\n+++ b/Products/CMFPlone/browser/templates/plone-addsite.pt\n@@ -24,9 +24,9 @@\n \n
\n
\n-

Plone logo

\n
\ndiff --git a/Products/CMFPlone/browser/templates/plone-admin-logged-out.pt b/Products/CMFPlone/browser/templates/plone-admin-logged-out.pt\nindex b12de5f1e0..da0752d9ba 100644\n--- a/Products/CMFPlone/browser/templates/plone-admin-logged-out.pt\n+++ b/Products/CMFPlone/browser/templates/plone-admin-logged-out.pt\n@@ -23,9 +23,9 @@\n \n
\n
\n-

Plone logo

\n

You are now logged out.

\ndiff --git a/Products/CMFPlone/browser/templates/plone-overview.pt b/Products/CMFPlone/browser/templates/plone-overview.pt\nindex b1a159b988..2e1b2e50d3 100644\n--- a/Products/CMFPlone/browser/templates/plone-overview.pt\n+++ b/Products/CMFPlone/browser/templates/plone-overview.pt\n@@ -24,11 +24,11 @@\n many python:len(sites) > 1;">\n
\n
\n-

Plone logo

\n+

Plone logo

\n

Plone is up and running.

\n

\n For an introduction to Plone, success stories, demos, providers, visit\ndiff --git a/Products/CMFPlone/browser/templates/plone-upgrade.pt b/Products/CMFPlone/browser/templates/plone-upgrade.pt\nindex 21b66bbe9e..3d899aa99c 100644\n--- a/Products/CMFPlone/browser/templates/plone-upgrade.pt\n+++ b/Products/CMFPlone/browser/templates/plone-upgrade.pt\n@@ -25,9 +25,9 @@\n \n

\n
\n-

Plone logo

\n

\ndiff --git a/Products/CMFPlone/tests/test_utils.py b/Products/CMFPlone/tests/test_utils.py\nindex 17e66a96fc..eb30c720bf 100644\n--- a/Products/CMFPlone/tests/test_utils.py\n+++ b/Products/CMFPlone/tests/test_utils.py\n@@ -43,13 +43,22 @@ def test_getSiteLogo_with_setting(self):\n registry = getUtility(IRegistry)\n settings = registry.forInterface(ISiteSchema, prefix=\'plone\')\n settings.site_logo = SITE_LOGO_BASE64\n+ logo_url, logo_type = getSiteLogo(include_type=True)\n \n self.assertTrue(\n \'http://nohost/plone/@@site-logo/pixel.png\'\n- in getSiteLogo())\n+ in logo_url)\n+\n+ self.assertEqual(\n+ "image/png", logo_type\n+ )\n \n def test_getSiteLogo_with_no_setting(self):\n from Products.CMFPlone.utils import getSiteLogo\n+ logo_url, logo_type = getSiteLogo(include_type=True)\n self.assertTrue(\n- \'http://nohost/plone/logo.png\'\n- in getSiteLogo())\n+ \'http://nohost/plone/++resource++plone-logo.svg\'\n+ in logo_url)\n+ self.assertEqual(\n+ "image/svg+xml", logo_type\n+ )\ndiff --git a/Products/CMFPlone/utils.py b/Products/CMFPlone/utils.py\nindex e94f7446ea..c92cae5786 100644\n--- a/Products/CMFPlone/utils.py\n+++ b/Products/CMFPlone/utils.py\n@@ -596,9 +596,11 @@ def getHighPixelDensityScales():\n return func()\n \n \n-def getSiteLogo(site=None):\n+def getSiteLogo(site=None, include_type=False):\n from plone.base.interfaces import ISiteSchema\n from plone.formwidget.namedfile.converter import b64decode_file\n+ import mimetypes\n+\n if site is None:\n site = getSite()\n registry = getUtility(IRegistry)\n@@ -607,10 +609,17 @@ def getSiteLogo(site=None):\n \n if getattr(settings, \'site_logo\', False):\n filename, data = b64decode_file(settings.site_logo)\n- return \'{}/@@site-logo/{}\'.format(\n+ site_logo_url = \'{}/@@site-logo/{}\'.format(\n site_url, filename)\n- return \'%s/logo.png\' % site_url\n+ site_logo_type = mimetypes.guess_type(filename)[0]\n+ else:\n+ site_logo_url = \'%s/++resource++plone-logo.svg\' % site_url\n+ site_logo_type = "image/svg+xml"\n+\n+ if not include_type:\n+ return site_logo_url\n \n+ return (site_logo_url, site_logo_type)\n \n \n def _safe_format(inst, method):\ndiff --git a/news/3558.feature b/news/3558.feature\nnew file mode 100644\nindex 0000000000..e7bec0e2a1\n--- /dev/null\n+++ b/news/3558.feature\n@@ -0,0 +1,2 @@\n+SVG image as default Plone logo.\n+[petschki]\n' +b'diff --git a/plone/outputfilters/browser/resolveuid.py b/plone/outputfilters/browser/resolveuid.py\nindex e774de2..7883fd1 100644\n--- a/plone/outputfilters/browser/resolveuid.py\n+++ b/plone/outputfilters/browser/resolveuid.py\n@@ -2,6 +2,7 @@\n from Acquisition import aq_base\n from Products.CMFCore.utils import getToolByName\n from zExceptions import NotFound\n+from zope.deprecation import deprecate\n from zope.interface import implementer\n from zope.publisher.browser import BrowserView\n from zope.publisher.interfaces import IPublishTraverse\n@@ -13,6 +14,7 @@\n from zope.app.component.hooks import getSite\n \n \n+@deprecate("Please use plone.app.uuid.utils.uuidToURL instead.")\n def uuidToURL(uuid):\n """Resolves a UUID to a URL via the UID index of portal_catalog.\n """\n@@ -22,6 +24,10 @@ def uuidToURL(uuid):\n return res[0].getURL()\n \n \n+@deprecate(\n+ "Please use plone.app.uuid.utils.uuidToObject instead. "\n+ "But be aware that this does an extra security check."\n+)\n def uuidToObject(uuid):\n """Resolves a UUID to an object via the UID index of portal_catalog.\n """\ndiff --git a/setup.py b/setup.py\nindex 8cecaca..052dc3f 100644\n--- a/setup.py\n+++ b/setup.py\n@@ -71,6 +71,7 @@ def read(filename):\n \'setuptools\',\n \'six\',\n \'unidecode\',\n+ \'zope.deprecation\',\n \'beautifulsoup4\',\n \'lxml\',\n ],\n' -Repository: Products.CMFPlone +Repository: plone.outputfilters Branch: refs/heads/master -Date: 2022-06-16T18:54:19+02:00 +Date: 2022-06-10T13:36:13+02:00 Author: Maurits van Rees (mauritsvanrees) -Commit: https://github.com/plone/Products.CMFPlone/commit/c7433f66ca4f1902c64b19668eba1b4c99734cb5 +Commit: https://github.com/plone/plone.outputfilters/commit/10651b93d002e9a9fa26faab93c1ab678e4eeaa8 + +Removed _shorttag_replace from picture_variants filter. + +This was copied from the resolve_uid_and_caption filter, but was missing the singleton_tags definition. -Merge pull request #3477 from plone/mrtango-image-handling-sourcesets-settings +I don't think it is needed in resolve_uid_and_caption either. +It is meant to replace singleton tags without any content which should not be singleton tags. +For example change `<p />` into `<p></p>`. +But beautifulsoup is already doing that for us: -Add image srcset's configuration to TinyMCE pattern settings +``` +>>> print(BeautifulSoup("<p />", "html.parser")) +<p></p> +``` + +Cleanup (plus possibly adding a test) can be done later, to not overfill this PR. + +Files changed: +M plone/outputfilters/filters/picture_variants.py + +b'diff --git a/plone/outputfilters/filters/picture_variants.py b/plone/outputfilters/filters/picture_variants.py\nindex 935c3d2..d3d2bb6 100644\n--- a/plone/outputfilters/filters/picture_variants.py\n+++ b/plone/outputfilters/filters/picture_variants.py\n@@ -1,5 +1,4 @@\n import logging\n-import re\n \n from bs4 import BeautifulSoup\n from plone.outputfilters.interfaces import IFilter\n@@ -17,13 +16,6 @@ class PictureVariantsFilter(object):\n \n order = 700\n \n- def _shorttag_replace(self, match):\n- tag = match.group(1)\n- if tag in self.singleton_tags:\n- return "<" + tag + " />"\n- else:\n- return "<" + tag + ">"\n-\n def is_enabled(self):\n if self.context is None:\n return False\n@@ -38,7 +30,6 @@ def __init__(self, context=None, request=None):\n \n \n def __call__(self, data):\n- data = re.sub(r"<([^<>\\s]+?)\\s*/>", self._shorttag_replace, data)\n soup = BeautifulSoup(safe_nativestring(data), "html.parser")\n \n for elem in soup.find_all("img"):\n' + +Repository: plone.outputfilters + + +Branch: refs/heads/master +Date: 2022-06-10T17:27:28+02:00 +Author: Maurits van Rees (mauritsvanrees) +Commit: https://github.com/plone/plone.outputfilters/commit/73af19d15cb1eaae72143bcd454cdc129eeed0a1 + +Use our own version of uuidToObject. + +The one from plone.app.uuid does an extra security check. +We *should* be using that one, instead of having our own slightly different copy. +But let's do that in a later PR, where we can focus on what might possibly break because of this. +See https://github.com/plone/plone.outputfilters/issues/52 + +Files changed: +M plone/outputfilters/filters/resolveuid_and_caption.py + +b'diff --git a/plone/outputfilters/filters/resolveuid_and_caption.py b/plone/outputfilters/filters/resolveuid_and_caption.py\nindex b48c07d..014cd14 100644\n--- a/plone/outputfilters/filters/resolveuid_and_caption.py\n+++ b/plone/outputfilters/filters/resolveuid_and_caption.py\n@@ -6,7 +6,7 @@\n from bs4 import BeautifulSoup\n from DocumentTemplate.html_quote import html_quote\n from DocumentTemplate.DT_Var import newline_to_br\n-from plone.app.uuid.utils import uuidToObject\n+from plone.outputfilters.browser.resolveuid import uuidToObject\n from plone.outputfilters.interfaces import IFilter\n from plone.registry.interfaces import IRegistry\n from Products.CMFCore.interfaces import IContentish\n' + +Repository: plone.outputfilters + + +Branch: refs/heads/master +Date: 2022-06-16T14:21:48+02:00 +Author: Philip Bauer (pbauer) +Commit: https://github.com/plone/plone.outputfilters/commit/397c7e74f34c1c2426231049a67f26b811c1b4da + +fix test Files changed: -A news/3477.feature -M Products/CMFPlone/patterns/settings.py -M Products/CMFPlone/tests/browser_collection_views.txt +M plone/outputfilters/tests/test_picture_variants.py + +b'diff --git a/plone/outputfilters/tests/test_picture_variants.py b/plone/outputfilters/tests/test_picture_variants.py\nindex 542c161..2307eef 100644\n--- a/plone/outputfilters/tests/test_picture_variants.py\n+++ b/plone/outputfilters/tests/test_picture_variants.py\n@@ -173,7 +173,7 @@ def test_parsing_long_doc(self):\n

\n \n \n+ srcset="resolveuid/{uid}/@@images/image/preview 400w, resolveuid/{uid}/@@images/image/large 800w, resolveuid/{uid}/@@images/image/larger 1000w"/>\n \n' + +Repository: plone.outputfilters -b'diff --git a/Products/CMFPlone/patterns/settings.py b/Products/CMFPlone/patterns/settings.py\nindex 2d63b3ef69..a8fe05d5a2 100644\n--- a/Products/CMFPlone/patterns/settings.py\n+++ b/Products/CMFPlone/patterns/settings.py\n@@ -7,6 +7,7 @@\n from plone.registry.interfaces import IRegistry\n from plone.uuid.interfaces import IUUID\n from Products.CMFCore.interfaces._content import IFolderish\n+from plone.base.interfaces import IImagingSchema\n from plone.base.interfaces import ILinkSchema\n from plone.base.interfaces import IPatternsSettings\n from plone.base.interfaces import IPloneSiteRoot\n@@ -72,12 +73,32 @@ def mark_special_links(self):\n \n @property\n def image_scales(self):\n+ # Keep image_scales at least until https://github.com/plone/mockup/pull/1156\n+ # is merged and plone.staticresources is updated.\n factory = getUtility(IVocabularyFactory, "plone.app.vocabularies.ImagesScales")\n vocabulary = factory(self.context)\n ret = [{"title": translate(it.title), "value": it.value} for it in vocabulary]\n ret = sorted(ret, key=lambda it: it["title"])\n return json.dumps(ret)\n \n+ @property\n+ def picture_variants(self):\n+ registry = getUtility(IRegistry)\n+ settings = registry.forInterface(IImagingSchema, prefix="plone", check=False)\n+ editor_picture_variants = {}\n+ for k, picture_variant in settings.picture_variants.items():\n+ hide_in_editor = picture_variant.get("hideInEditor")\n+ if hide_in_editor:\n+ continue\n+ editor_picture_variants[k] = picture_variant\n+ return editor_picture_variants\n+\n+ @property\n+ def image_captioning(self):\n+ registry = getUtility(IRegistry)\n+ settings = registry.forInterface(IImagingSchema, prefix="plone", check=False)\n+ return settings.image_captioning\n+\n def tinymce(self):\n """\n data-pat-tinymce : JSON.stringify({\n@@ -129,7 +150,11 @@ def tinymce(self):\n configuration = {\n "base_url": self.context.absolute_url(),\n "imageTypes": image_types,\n+ # Keep imageScales at least until https://github.com/plone/mockup/pull/1156\n+ # is merged and plone.staticresources is updated.\n "imageScales": self.image_scales,\n+ "pictureVariants": self.picture_variants,\n+ "imageCaptioningEnabled": self.image_captioning,\n "linkAttribute": "UID",\n # This is for loading the languages on tinymce\n "loadingBaseUrl": "{}/++plone++static/components/tinymce-builded/"\ndiff --git a/Products/CMFPlone/tests/browser_collection_views.txt b/Products/CMFPlone/tests/browser_collection_views.txt\nindex 1410ef8387..c2458703d0 100644\n--- a/Products/CMFPlone/tests/browser_collection_views.txt\n+++ b/Products/CMFPlone/tests/browser_collection_views.txt\n@@ -48,6 +48,12 @@ Now let\'s login and visit the collection in the test browser:\n >>> browser.getControl(\'Log in\').click()\n >>> browser.open(\'http://nohost/plone/folder/collection\')\n \n+When checking if the collection text is in the output, we are not interested in differences in whitespace.\n+So we use a normalize function:\n+\n+ >>> def normalize(value):\n+ ... return value.translate(str.maketrans({" ": None, "\\n": None, "\\t": None, "\\r": None}))\n+\n Lets check the listing_view (Standard view):\n \n >>> browser.getLink(\'Standard view\').click()\n@@ -55,7 +61,7 @@ Lets check the listing_view (Standard view):\n True\n >>> collection_description in browser.contents\n True\n- >>> collection_text in browser.contents\n+ >>> normalize(collection_text) in normalize(browser.contents)\n True\n \n Lets check the summary_view (Summary view):\n@@ -65,7 +71,7 @@ Lets check the summary_view (Summary view):\n True\n >>> collection_description in browser.contents\n True\n- >>> collection_text in browser.contents\n+ >>> normalize(collection_text) in normalize(browser.contents)\n True\n \n Lets check the full_view (All content):\n@@ -75,7 +81,7 @@ Lets check the full_view (All content):\n True\n >>> collection_description in browser.contents\n True\n- >>> collection_text in browser.contents\n+ >>> normalize(collection_text) in normalize(browser.contents)\n True\n \n Lets check the tabular_view (Tabular view):\n@@ -86,7 +92,7 @@ Lets check the tabular_view (Tabular view):\n True\n >>> collection_description in browser.contents\n True\n- >>> collection_text in browser.contents\n+ >>> normalize(collection_text) in normalize(browser.contents)\n True\n \n Lets ensure that the text field is not requested on a folder. We\n@@ -110,5 +116,5 @@ Lets ensure text is shown when Collection is default view of a folder\n True\n >>> collection_description in browser.contents\n True\n- >>> collection_text in browser.contents\n+ >>> normalize(collection_text) in normalize(browser.contents)\n True\ndiff --git a/news/3477.feature b/news/3477.feature\nnew file mode 100644\nindex 0000000000..78e99ea9a1\n--- /dev/null\n+++ b/news/3477.feature\n@@ -0,0 +1 @@\n+Add image srcset\'s configuration to TinyMCE pattern settings [MrTango]\n\\ No newline at end of file\n' + +Branch: refs/heads/master +Date: 2022-06-16T18:53:51+02:00 +Author: Maurits van Rees (mauritsvanrees) +Commit: https://github.com/plone/plone.outputfilters/commit/f28f92e73546dd6ec2e32561df2c04305f91e7dd + +Merge pull request #49 from plone/mrtango-image-sourcesets-filter + +Add image sourcesets filter + +Files changed: +A news/49.feature +A plone/outputfilters/filters/picture_variants.py +A plone/outputfilters/tests/test_picture_variants.py +M plone/outputfilters/README.rst +M plone/outputfilters/browser/captioned_image.pt +M plone/outputfilters/browser/resolveuid.py +M plone/outputfilters/filters/configure.zcml +M plone/outputfilters/filters/resolveuid_and_caption.py +M plone/outputfilters/tests/test_resolveuid_and_caption.py +M setup.py + +b'diff --git a/news/49.feature b/news/49.feature\nnew file mode 100644\nindex 0000000..ce4f04b\n--- /dev/null\n+++ b/news/49.feature\n@@ -0,0 +1 @@\n+Add image_srcset output filter, to convert IMG tags into PICTURE tags with multiple source definitions as define in imaging control panel [MrTango]\n\\ No newline at end of file\ndiff --git a/plone/outputfilters/README.rst b/plone/outputfilters/README.rst\nindex 454b166..31342f9 100644\n--- a/plone/outputfilters/README.rst\n+++ b/plone/outputfilters/README.rst\n@@ -62,7 +62,7 @@ be applied::\n >>> portal = layer[\'portal\']\n >>> str(portal.portal_transforms.convertTo(\'text/x-html-safe\',\n ... \'test--test\', mimetype=\'text/html\', context=portal))\n- \'test\xe2\x80\x94test\'\n+ \'test\xe2\x80\x94test\\n\'\n \n \n How it works\ndiff --git a/plone/outputfilters/browser/captioned_image.pt b/plone/outputfilters/browser/captioned_image.pt\nindex 8f4a9a8..637ba2a 100644\n--- a/plone/outputfilters/browser/captioned_image.pt\n+++ b/plone/outputfilters/browser/captioned_image.pt\n@@ -1,6 +1,4 @@\n

\n- [image goes here]\n+ \n
\n
\ndiff --git a/plone/outputfilters/browser/resolveuid.py b/plone/outputfilters/browser/resolveuid.py\nindex e774de2..7883fd1 100644\n--- a/plone/outputfilters/browser/resolveuid.py\n+++ b/plone/outputfilters/browser/resolveuid.py\n@@ -2,6 +2,7 @@\n from Acquisition import aq_base\n from Products.CMFCore.utils import getToolByName\n from zExceptions import NotFound\n+from zope.deprecation import deprecate\n from zope.interface import implementer\n from zope.publisher.browser import BrowserView\n from zope.publisher.interfaces import IPublishTraverse\n@@ -13,6 +14,7 @@\n from zope.app.component.hooks import getSite\n \n \n+@deprecate("Please use plone.app.uuid.utils.uuidToURL instead.")\n def uuidToURL(uuid):\n """Resolves a UUID to a URL via the UID index of portal_catalog.\n """\n@@ -22,6 +24,10 @@ def uuidToURL(uuid):\n return res[0].getURL()\n \n \n+@deprecate(\n+ "Please use plone.app.uuid.utils.uuidToObject instead. "\n+ "But be aware that this does an extra security check."\n+)\n def uuidToObject(uuid):\n """Resolves a UUID to an object via the UID index of portal_catalog.\n """\ndiff --git a/plone/outputfilters/filters/configure.zcml b/plone/outputfilters/filters/configure.zcml\nindex 19ef066..4376517 100644\n--- a/plone/outputfilters/filters/configure.zcml\n+++ b/plone/outputfilters/filters/configure.zcml\n@@ -3,6 +3,13 @@\n xmlns:zcml="http://namespaces.zope.org/zcml"\n xmlns:browser="http://namespaces.zope.org/browser">\n \n+ \n+\n \\s]+?)\\s*/>\', self._shorttag_replace, data)\n soup = BeautifulSoup(safe_unicode(data), \'html.parser\')\n-\n for elem in soup.find_all([\'a\', \'area\']):\n attributes = elem.attrs\n href = attributes.get(\'href\')\n@@ -159,8 +152,9 @@ def __call__(self, data):\n and not href.startswith(\'#\'):\n attributes[\'href\'] = self._render_resolveuid(href)\n for elem in soup.find_all([\'source\', \'img\']):\n- # SOURCE is used for video and audio.\n- # SRCSET specified multiple images (see below).\n+ # handles srcset attributes, not src\n+ # parent of SOURCE is picture here.\n+ # SRCSET on source/img specifies one or more images (see below).\n attributes = elem.attrs\n srcset = attributes.get(\'srcset\')\n if not srcset:\n@@ -169,10 +163,12 @@ def __call__(self, data):\n # [(src1, 480w), (src2, 360w)]\n srcs = [src.strip().split() for src in srcset.strip().split(\',\') if src.strip()]\n for idx, elm in enumerate(srcs):\n- srcs[idx][0] = self._render_resolveuid(elm[0])\n+ image_url = elm[0]\n+ image, fullimage, src, description = self.resolve_image(image_url)\n+ srcs[idx][0] = src\n attributes[\'srcset\'] = \',\'.join(\' \'.join(src) for src in srcs)\n for elem in soup.find_all([\'source\', \'iframe\', \'audio\', \'video\']):\n- # SOURCE is used for video and audio.\n+ # parent of SOURCE is video or audio here.\n # AUDIO/VIDEO can also have src attribute.\n # IFRAME is used to embed PDFs.\n attributes = elem.attrs\n@@ -180,42 +176,70 @@ def __call__(self, data):\n if not src:\n continue\n attributes[\'src\'] = self._render_resolveuid(src)\n- for elem in soup.find_all(\'img\'):\n- attributes = elem.attrs\n+ for elem in soup.find_all([\'img\', \'picture\']):\n+ if elem.name == "picture":\n+ img_elem = elem.find("img")\n+ else:\n+ img_elem = elem\n+ # handle src attribute\n+ attributes = img_elem.attrs\n src = attributes.get(\'src\', \'\')\n image, fullimage, src, description = self.resolve_image(src)\n attributes["src"] = src\n-\n+ if image and hasattr(image, "width"):\n+ if "width" not in attributes:\n+ attributes["width"] = image.width\n+ if "height" not in attributes:\n+ attributes["height"] = image.height\n if fullimage is not None:\n # Check to see if the alt / title tags need setting\n title = safe_unicode(aq_acquire(fullimage, \'Title\')())\n if not attributes.get(\'alt\'):\n- # XXX alt attribute contains *alternate* text\n- attributes[\'alt\'] = description or title\n+ # bettr an emty alt tag than none. This avoid\'s screen readers\n+ # to read the file name instead. A better fallback would be\n+ # a fallback alt text comming from the img object.\n+ attributes[\'alt\'] = ""\n if \'title\' not in attributes:\n attributes[\'title\'] = title\n \n- caption = description\n- # Check if the image needs to be captioned\n- if (\n- self.captioned_images and\n- image is not None and\n- caption and\n- \'captioned\' in attributes.get(\'class\', [])\n- ):\n- self.handle_captioned_image(\n- attributes, image, fullimage, elem, caption)\n+ # handle captions\n+ if \'captioned\' in elem.attrs.get(\'class\', []):\n+ caption = description\n+ caption_manual_override = attributes.get("data-captiontext", "")\n+ if caption_manual_override:\n+ caption = caption_manual_override\n+ # Check if the image needs to be captioned\n+ if (self.captioned_images and caption):\n+ options = {}\n+ options["tag"] = elem.prettify()\n+ options["caption"] = newline_to_br(html_quote(caption))\n+ options["class"] = \' \'.join(attributes[\'class\'])\n+ del attributes[\'class\']\n+ if elem.name == "picture":\n+ elem.append(img_elem)\n+ captioned = BeautifulSoup(\n+ self.captioned_image_template(**options), \'html.parser\')\n+\n+ # if we are a captioned image within a link, remove and occurrences\n+ # of a tags inside caption template to preserve the outer link\n+ if bool(elem.find_parent(\'a\')) and bool(captioned.find(\'a\')):\n+ captioned.a.unwrap()\n+ if elem.name == "picture":\n+ del captioned.picture.img["class"]\n+ else:\n+ del captioned.img["class"]\n+ elem.replace_with(captioned)\n return six.text_type(soup)\n \n- def lookup_uid(self, uid):\n- context = self.context\n- if HAS_LINGUAPLONE:\n- # If we have LinguaPlone installed, add support for language-aware\n- # references\n- uids = translated_references(context, context.Language(), uid)\n- if len(uids) > 0:\n- uid = uids[0]\n- return uuidToObject(uid)\n+ def resolve_scale_data(self, url):\n+ """ return scale url, width and height\n+ """\n+ url_parts = url.split("/")\n+ field_name = url_parts[-2]\n+ scale_name = url_parts[-1]\n+ obj, subpath, appendix = self.resolve_link(url)\n+ scale_view = obj.unrestrictedTraverse(\'@@images\', None)\n+ return scale_view.scale(field_name, scale_name, pre=True)\n \n def resolve_link(self, href):\n obj = None\n@@ -231,7 +255,7 @@ def resolve_link(self, href):\n match = resolveuid_re.match(subpath)\n if match is not None:\n uid, _subpath = match.groups()\n- obj = self.lookup_uid(uid)\n+ obj = uuidToObject(uid)\n if obj is not None:\n subpath = _subpath\n \n@@ -242,7 +266,6 @@ def resolve_image(self, src):\n if urlsplit(src)[0]:\n # We have a scheme\n return None, None, src, description\n-\n base = self.context\n subpath = src\n appendix = \'\'\n@@ -254,14 +277,15 @@ def traversal_stack(base, path):\n obj = base\n stack = [obj]\n components = path.split(\'/\')\n+ # print("components: {}".format(components))\n while components:\n child_id = unquote(components.pop(0))\n try:\n if hasattr(aq_base(obj), \'scale\'):\n if components:\n- child = obj.scale(child_id, components.pop())\n+ child = obj.scale(child_id, components.pop(), pre=True)\n else:\n- child = obj.scale(child_id)\n+ child = obj.scale(child_id, pre=True)\n else:\n # Do not use restrictedTraverse here; the path to the\n # image may lead over containers that lack the View\n@@ -276,6 +300,7 @@ def traversal_stack(base, path):\n return\n obj = child\n stack.append(obj)\n+ # print(f"traversal_stack: {stack}")\n return stack\n \n def traverse_path(base, path):\n@@ -288,7 +313,11 @@ def traverse_path(base, path):\n if obj is not None:\n # resolved uid\n fullimage = obj\n- image = traverse_path(fullimage, subpath)\n+ image = None\n+ if not subpath:\n+ image = traverse_path(fullimage, "@@images/image")\n+ if image is None:\n+ image = traverse_path(fullimage, subpath)\n elif \'/@@\' in subpath:\n # split on view\n pos = subpath.find(\'/@@\')\n@@ -309,10 +338,13 @@ def traverse_path(base, path):\n if hasattr(aq_base(parent), \'tag\'):\n fullimage = parent\n break\n+ if not hasattr(image, "width"):\n+ image_scale = traverse_path(image, "@@images/image")\n+ if image_scale:\n+ image = image_scale\n \n if image is None:\n return None, None, src, description\n-\n try:\n url = image.absolute_url()\n except AttributeError:\n@@ -320,58 +352,3 @@ def traverse_path(base, path):\n src = url + appendix\n description = safe_unicode(aq_acquire(fullimage, \'Description\')())\n return image, fullimage, src, description\n-\n- def handle_captioned_image(self, attributes, image, fullimage,\n- elem, caption):\n- """Handle captioned image.\n-\n- The img element is replaced by a definition list\n- as created by the template ../browser/captioned_image.pt\n- """\n- klass = \' \'.join(attributes[\'class\'])\n- del attributes[\'class\']\n- del attributes[\'src\']\n- if \'width\' in attributes and attributes[\'width\']:\n- attributes[\'width\'] = int(attributes[\'width\'])\n- if \'height\' in attributes and attributes[\'height\']:\n- attributes[\'height\'] = int(attributes[\'height\'])\n- view = fullimage.unrestrictedTraverse(\'@@images\', None)\n- if view is not None:\n- original_width, original_height = view.getImageSize()\n- else:\n- original_width, original_height = fullimage.width, fullimage.height\n- if image is not fullimage:\n- # image is a scale object\n- tag = image.tag\n- width = image.width\n- else:\n- if hasattr(aq_base(image), \'tag\'):\n- tag = image.tag\n- else:\n- tag = view.scale().tag\n- width = original_width\n- options = {\n- \'class\': klass,\n- \'originalwidth\': attributes.get(\'width\', None),\n- \'originalalt\': attributes.get(\'alt\', None),\n- \'url_path\': fullimage.absolute_url_path(),\n- \'caption\': newline_to_br(html_quote(caption)),\n- \'image\': image,\n- \'fullimage\': fullimage,\n- \'tag\': tag(**attributes),\n- \'isfullsize\': image is fullimage or (\n- image.width == original_width and\n- image.height == original_height),\n- \'width\': attributes.get(\'width\', width),\n- }\n-\n- captioned = BeautifulSoup(\n- self.captioned_image_template(**options), \'html.parser\')\n-\n- # if we are a captioned image within a link, remove and occurrences\n- # of a tags inside caption template to preserve the outer link\n- if bool(elem.find_parent(\'a\')) and \\\n- bool(captioned.find(\'a\')):\n- captioned.a.unwrap()\n-\n- elem.replace_with(captioned)\ndiff --git a/plone/outputfilters/tests/test_picture_variants.py b/plone/outputfilters/tests/test_picture_variants.py\nnew file mode 100644\nindex 0000000..2307eef\n--- /dev/null\n+++ b/plone/outputfilters/tests/test_picture_variants.py\n@@ -0,0 +1,248 @@\n+# -*- coding: utf-8 -*-\n+from doctest import _ellipsis_match\n+from doctest import OutputChecker\n+from doctest import REPORT_NDIFF\n+from os.path import abspath\n+from os.path import dirname\n+from os.path import join\n+from plone.app.testing import setRoles\n+from plone.app.testing import TEST_USER_ID\n+from plone.app.testing.bbb import PloneTestCase\n+from plone.namedfile.file import NamedBlobImage\n+from plone.namedfile.file import NamedImage\n+from plone.namedfile.tests.test_scaling import DummyContent as NFDummyContent\n+from plone.outputfilters.filters.picture_variants import PictureVariantsFilter\n+from plone.outputfilters.testing import PLONE_OUTPUTFILTERS_FUNCTIONAL_TESTING\n+from Products.PortalTransforms.tests.utils import normalize_html\n+\n+\n+PREFIX = abspath(dirname(__file__))\n+\n+\n+def dummy_image():\n+ filename = join(PREFIX, u\'image.jpg\')\n+ data = None\n+ with open(filename, \'rb\') as fd:\n+ data = fd.read()\n+ fd.close()\n+ return NamedBlobImage(data=data, filename=filename)\n+\n+\n+class PictureVariantsFilterIntegrationTestCase(PloneTestCase):\n+\n+ layer = PLONE_OUTPUTFILTERS_FUNCTIONAL_TESTING\n+\n+ image_id = \'image.jpg\'\n+\n+ def _makeParser(self, **kw):\n+ parser = PictureVariantsFilter(context=self.portal)\n+ for k, v in kw.items():\n+ setattr(parser, k, v)\n+ return parser\n+\n+ def _makeDummyContent(self):\n+ from OFS.SimpleItem import SimpleItem\n+\n+ class DummyContent(SimpleItem):\n+\n+ def __init__(self, id):\n+ self.id = id\n+\n+ def UID(self):\n+ return \'foo\'\n+\n+ allowedRolesAndUsers = (\'Anonymous\',)\n+\n+ class DummyContent2(NFDummyContent):\n+ id = __name__ = \'foo2\'\n+ title = u\'Sch\xc3\xb6nes Bild\'\n+\n+ def UID(self):\n+ return \'foo2\'\n+\n+ dummy = DummyContent(\'foo\')\n+ self.portal._setObject(\'foo\', dummy)\n+ self.portal.portal_catalog.catalog_object(self.portal.foo)\n+\n+ dummy2 = DummyContent2(\'foo2\')\n+ with open(join(PREFIX, self.image_id), \'rb\') as fd:\n+ data = fd.read()\n+ fd.close()\n+ dummy2.image = NamedImage(data, \'image/jpeg\', u\'image.jpeg\')\n+ self.portal._setObject(\'foo2\', dummy2)\n+ self.portal.portal_catalog.catalog_object(self.portal.foo2)\n+\n+ def _assertTransformsTo(self, input, expected):\n+ # compare two chunks of HTML ignoring whitespace differences,\n+ # and with a useful diff on failure\n+ out = self.parser(input)\n+ normalized_out = normalize_html(out)\n+ normalized_expected = normalize_html(expected)\n+ # print("\\n e: {}".format(expected))\n+ # print("\\n o: {}".format(out))\n+ try:\n+ self.assertTrue(_ellipsis_match(normalized_expected,\n+ normalized_out))\n+ except AssertionError:\n+ class wrapper(object):\n+ want = expected\n+ raise AssertionError(self.outputchecker.output_difference(\n+ wrapper, out, REPORT_NDIFF))\n+\n+ def afterSetUp(self):\n+ # create an image and record its UID\n+ setRoles(self.portal, TEST_USER_ID, [\'Manager\'])\n+\n+ if self.image_id not in self.portal:\n+ self.portal.invokeFactory(\n+ \'Image\', id=self.image_id, title=\'Image\')\n+ image = self.portal[self.image_id]\n+ image.setDescription(\'My caption\')\n+ image.image = dummy_image()\n+ image.reindexObject()\n+ self.UID = image.UID()\n+ self.parser = self._makeParser(captioned_images=True,\n+ resolve_uids=True)\n+ assert self.parser.is_enabled()\n+\n+ self.outputchecker = OutputChecker()\n+\n+ def beforeTearDown(self):\n+ self.login()\n+ setRoles(self.portal, TEST_USER_ID, [\'Manager\'])\n+ del self.portal[self.image_id]\n+\n+ def test_parsing_minimal(self):\n+ text = """
\n+ Some simple text.\n+
"""\n+ res = self.parser(text)\n+ self.assertEqual(text, str(res))\n+\n+ def test_parsing_long_doc(self):\n+ text = """

Welcome!

\n+

If you\'re seeing this instead of the web site you were expecting, the owner of this web site has\n+ just installed Plone. Do not contact the Plone Team or the Plone support channels about this.

\n+

\n+

Get started

\n+

Before you start exploring your newly created Plone site, please do the following:

\n+
    \n+
  1. Make sure you are logged in as an admin/manager user. (You should have a Site Setup entry\n+ in the user menu)
  2. \n+
\n+

Get comfortable

\n+

After that, we suggest you do one or more of the following:

\n+

\n+

Make it your own

\n+

Plone has a lot of different settings that can be used to make it do what you want it to. Some examples:

\n+

Tell us how you use it

\n+

Are you doing something interesting with Plone? Big site deployments, interesting use cases? Do you have a company\n+ that delivers Plone-based solutions?

\n+

Find out more about Plone

\n+

\n+

Plone is a powerful content management system built on a rock-solid application stack written using the Python\n+ programming language. More about these technologies:

\n+

\n+

Support the Plone Foundation

\n+

Plone is made possible only through the efforts of thousands of dedicated individuals and hundreds of companies. The\n+ Plone Foundation:

\n+
    \n+
  • \xe2\x80\xa6protects and promotes Plone.
  • \n+
  • \xe2\x80\xa6is a registered 501(c)(3) charitable organization.
  • \n+
  • \xe2\x80\xa6donations are tax-deductible.
  • \n+
\n+

Thanks for using our product; we hope you like it!

\n+

\xe2\x80\x94The Plone Team

\n+ """.format(uid=self.UID)\n+ import time\n+ startTime = time.time()\n+ res = self.parser(text)\n+ executionTime = (time.time() - startTime)\n+ print("\\n\\nimage srcset parsing time: {}\\n".format(executionTime))\n+ self.assertTrue(res)\n+\n+ text_out = """

Welcome!

\n+

If you\'re seeing this instead of the web site you were expecting, the owner of this web site has\n+ just installed Plone. Do not contact the Plone Team or the Plone support channels about this.

\n+

\n+ \n+ \n+ \n+ \n+

\n+

Get started

\n+

Before you start exploring your newly created Plone site, please do the following:

\n+
    \n+
  1. Make sure you are logged in as an admin/manager user.(You should have a Site Setup entry\n+ in the user menu)
  2. \n+
\n+

Get comfortable

\n+

After that, we suggest you do one or more of the following:

\n+

\n+ \n+ \n+ \n+ \n+

\n+

Make it your own

\n+

Plone has a lot of different settings that can be used to make it do what you want it to. Some examples:

\n+

Tell us how you use it

\n+

Are you doing something interesting with Plone? Big site deployments, interesting use cases? Do you have a company\n+ that delivers Plone-based solutions?

\n+

Find out more about Plone

\n+

\n+ \n+ \n+ \n+ \n+

\n+

Plone is a powerful content management system built on a rock-solid application stack written using the Python\n+ programming language. More about these technologies:

\n+

\n+ \n+ \n+ \n+ \n+

\n+

Support the Plone Foundation

\n+

Plone is made possible only through the efforts of thousands of dedicated individuals and hundreds of companies. The\n+ Plone Foundation:

\n+
    \n+
  • \xe2\x80\xa6protects and promotes Plone.
  • \n+
  • \xe2\x80\xa6is a registered 501(c)(3) charitable organization.
  • \n+
  • \xe2\x80\xa6donations are tax-deductible.
  • \n+
\n+

Thanks for using our product; we hope you like it!

\n+

\xe2\x80\x94The Plone Team

\n+ """.format(uid=self.UID)\n+ self._assertTransformsTo(text, text_out)\n+\n+ def test_parsing_with_nonexisting_srcset(self):\n+ text = """\n+

\n+ """.format(uid=self.UID)\n+ res = self.parser(text)\n+ self.assertTrue(res)\n+ text_out = """\n+

\n+ """.format(uid=self.UID)\n+ # verify that tag was not converted:\n+ self.assertTrue("data-picturevariant" in res)\n\\ No newline at end of file\ndiff --git a/plone/outputfilters/tests/test_resolveuid_and_caption.py b/plone/outputfilters/tests/test_resolveuid_and_caption.py\nindex ec748f5..b250951 100644\n--- a/plone/outputfilters/tests/test_resolveuid_and_caption.py\n+++ b/plone/outputfilters/tests/test_resolveuid_and_caption.py\n@@ -72,12 +72,17 @@ def UID(self):\n self.portal._setObject(\'foo2\', dummy2)\n self.portal.portal_catalog.catalog_object(self.portal.foo2)\n \n- def _assertTransformsTo(self, input, expected):\n+ def _assertTransformsTo(self, input, expected, parsing=True):\n # compare two chunks of HTML ignoring whitespace differences,\n # and with a useful diff on failure\n- out = self.parser(input)\n+ if parsing:\n+ out = self.parser(input)\n+ else:\n+ out = input\n normalized_out = normalize_html(out)\n normalized_expected = normalize_html(expected)\n+ # print("e: {}".format(normalized_expected))\n+ # print("o: {}".format(normalized_out))\n try:\n self.assertTrue(_ellipsis_match(normalized_expected,\n normalized_out))\n@@ -115,6 +120,71 @@ def test_parsing_minimal(self):\n res = self.parser(text)\n self.assertEqual(text, str(res))\n \n+ def test_parsing_long_doc(self):\n+ text = """
\n+

Welcome!

\n+

Learn more about Plone

\n+
\n+

If you\'re seeing this instead of the web site you were expecting, the owner of this web site has just installed Plone. Do not contact the Plone Team or the Plone support channels about this.

\n+

\n+

Get started

\n+

Before you start exploring your newly created Plone site, please do the following:

\n+
    \n+
  1. Make sure you are logged in as an admin/manager user. (You should have a Site Setup entry in the user menu)
  2. \n+
  3. Set up your mail server. (Plone needs a valid SMTP server to verify users and send out password reminders)
  4. \n+
  5. Decide what security level you want on your site. (Allow self registration, password policies, etc)
  6. \n+
\n+

Get comfortable

\n+

After that, we suggest you do one or more of the following:

\n+\n+

\n+

Make it your own

\n+

Plone has a lot of different settings that can be used to make it do what you want it to. Some examples:

\n+\n+

Tell us how you use it

\n+

Are you doing something interesting with Plone? Big site deployments, interesting use cases? Do you have a company that delivers Plone-based solutions?

\n+\n+

Find out more about Plone

\n+

Plone is a powerful content management system built on a rock-solid application stack written using the Python programming language. More about these technologies:

\n+\n+

\n+

Support the Plone Foundation

\n+

Plone is made possible only through the efforts of thousands of dedicated individuals and hundreds of companies. The Plone Foundation:

\n+\n+

Thanks for using our product; we hope you like it!

\n+

\xe2\x80\x94The Plone Team

\n+ """.format(uid=self.UID)\n+ import time\n+ startTime = time.time()\n+ res = self.parser(text)\n+ executionTime = (time.time() - startTime)\n+ print("\\n\\nresolve_uid_and_caption parsing time: {}\\n".format(executionTime))\n+ self.assertTrue(res)\n+\n def test_parsing_preserves_newlines(self):\n # Test if it preserves newlines which should not be filtered out\n text = """
This is line 1\n@@ -261,13 +331,12 @@ def test_image_captioning_in_news_item(self):\n         from plone.app.textfield.value import RichTextValue\n         news_item.text = RichTextValue(\n             \'\',\n-            \'text/html\', \'text/x-html-safe\')\n+            \'text/html\', \'text/html\')\n         news_item.setDescription("Description.")\n-\n         # Test captioning\n         output = news_item.text.output\n         text_out = """
\n-My caption\n+\n
My caption
\n
\n
"""\n@@ -275,13 +344,13 @@ def test_image_captioning_in_news_item(self):\n \n def test_image_captioning_absolutizes_uncaptioned_image(self):\n text_in = """"""\n- text_out = """My caption"""\n+ text_out = """"""\n self._assertTransformsTo(text_in, text_out)\n \n def test_image_captioning_absolute_path(self):\n text_in = """"""\n text_out = """
\n-My caption\n+\n
My caption
\n
"""\n self._assertTransformsTo(text_in, text_out)\n@@ -289,7 +358,7 @@ def test_image_captioning_absolute_path(self):\n def test_image_captioning_relative_path(self):\n text_in = """"""\n text_out = """
\n-My caption\n+\n
My caption
\n
"""\n self._assertTransformsTo(text_in, text_out)\n@@ -310,7 +379,7 @@ def test_image_captioning_relative_path_private_folder(self):\n \n text_in = """"""\n text_out = """
\n-My private image caption\n+\n
My private image caption
\n
"""\n self._assertTransformsTo(text_in, text_out)\n@@ -318,15 +387,15 @@ def test_image_captioning_relative_path_private_folder(self):\n def test_image_captioning_relative_path_scale(self):\n text_in = """"""\n text_out = """
\n-My caption\n+\n
My caption
\n
"""\n self._assertTransformsTo(text_in, text_out)\n \n- def test_image_captioning_resolveuid(self):\n+ def test_image_captioning_resolveuid_bare(self):\n text_in = """""" % self.UID\n text_out = """
\n-My caption\n+\n
My caption
\n
"""\n self._assertTransformsTo(text_in, text_out)\n@@ -334,7 +403,7 @@ def test_image_captioning_resolveuid(self):\n def test_image_captioning_resolveuid_scale(self):\n text_in = """""" % self.UID\n text_out = """
\n-My caption\n+\n
My caption
\n
"""\n self._assertTransformsTo(text_in, text_out)\n@@ -342,7 +411,7 @@ def test_image_captioning_resolveuid_scale(self):\n def test_image_captioning_resolveuid_new_scale(self):\n text_in = """""" % self.UID\n text_out = """
\n-My caption\n+\n
My caption
\n
"""\n self._assertTransformsTo(text_in, text_out)\n@@ -350,13 +419,13 @@ def test_image_captioning_resolveuid_new_scale(self):\n def test_image_captioning_resolveuid_new_scale_plone_namedfile(self):\n self._makeDummyContent()\n text_in = """"""\n- text_out = u"""Sch\xc3\xb6nes Bild"""\n+ text_out = u""""""\n self._assertTransformsTo(text_in, text_out)\n \n def test_image_captioning_resolveuid_no_scale(self):\n text_in = """""" % self.UID\n text_out = """
\n-My caption\n+\n
My caption
\n
"""\n self._assertTransformsTo(text_in, text_out)\n@@ -364,7 +433,7 @@ def test_image_captioning_resolveuid_no_scale(self):\n def test_image_captioning_resolveuid_with_srcset_and_src(self):\n text_in = """""" % (self.UID, self.UID, self.UID)\n text_out = """
\n-My caption\n+\n
My caption
\n
"""\n self._assertTransformsTo(text_in, text_out)\n@@ -384,20 +453,10 @@ def test_audio_resolveuid(self):\n text_out = """"""\n self._assertTransformsTo(text_in, text_out)\n \n- def test_source_resolveuid(self):\n- text_in = """""" % self.UID\n- text_out = """"""\n- self._assertTransformsTo(text_in, text_out)\n-\n- def test_source_resolveuid_srcset(self):\n- text_in = """""" % self.UID\n- text_out = """"""\n- self._assertTransformsTo(text_in, text_out)\n-\n def test_image_captioning_resolveuid_no_scale_plone_namedfile(self):\n self._makeDummyContent()\n text_in = """"""\n- text_out = u"""Sch\xc3\xb6nes Bild"""\n+ text_out = u""""""\n self._assertTransformsTo(text_in, text_out)\n \n def test_image_captioning_bad_uid(self):\n@@ -419,7 +478,7 @@ def test_image_captioning_external_url(self):\n def test_image_captioning_preserves_custom_attributes(self):\n text_in = """"""\n text_out = """
\n-My caption\n+\n
My caption
\n
"""\n self._assertTransformsTo(text_in, text_out)\n@@ -435,7 +494,7 @@ def test_image_captioning_handles_unquoted_attributes(self):\n def test_image_captioning_preserves_existing_links(self):\n text_in = """"""\n text_out = """
\n-My caption\n+\n
My caption
\n
\n
"""\n@@ -447,7 +506,7 @@ def test_image_captioning_handles_non_ascii(self):\n u\'Kupu Test Image \\xe5\\xe4\\xf6\')\n text_in = """"""\n text_out = u"""
\n-Kupu Test Image \\xe5\\xe4\\xf6\n+\n
Kupu Test Image \\xe5\\xe4\\xf6
\n
"""\n self._assertTransformsTo(text_in, text_out)\ndiff --git a/setup.py b/setup.py\nindex 4ad8816..052dc3f 100644\n--- a/setup.py\n+++ b/setup.py\n@@ -66,9 +66,12 @@ def read(filename):\n \'Products.GenericSetup\',\n \'Products.MimetypesRegistry\',\n \'Products.PortalTransforms>=2.0\',\n+ \'plone.namedfile\',\n+ \'plone.app.uuid\',\n \'setuptools\',\n \'six\',\n \'unidecode\',\n+ \'zope.deprecation\',\n \'beautifulsoup4\',\n \'lxml\',\n ],\n'