From 7471cf4618750a401df7abff1e6c7ea4e17172af Mon Sep 17 00:00:00 2001 From: Sebastian Mendel Date: Fri, 20 Feb 2026 14:07:07 +0100 Subject: [PATCH 1/2] feat: add CSS float and alignment support for images and figures Add missing float and alignment CSS for images and figures, replacing the deprecated HTML align attribute with CSS class mapping. - Add float-left/float-right CSS rules for img and figure elements with max-width: 50% and responsive breakpoint at 575.98px - Add align-center CSS for centered images and figures - Map :align: left/right/center to CSS classes in Twig templates, replacing the deprecated HTML align attribute on - Preserve :align: option on FigureNode (was being stripped) - Strip float classes from inner inside
to prevent caption wrapping issues - Add .clear-both CSS utility alias alongside existing .cc - Split ImagesAndFigures rendertest into Index, Zoom, and FloatAndAlignment pages --- .../ImagesAndFigures/FloatAndAlignment.rst | 147 ++++++++++ .../ImagesAndFigures/Index.rst | 253 +---------------- .../ImagesAndFigures/Zoom.rst | 266 ++++++++++++++++++ .../assets/sass/_utilities.scss | 3 +- .../assets/sass/components/_images.scss | 27 ++ .../assets/sass/components/_rst.scss | 16 ++ .../resources/public/css/theme.css | 47 +++- .../resources/template/body/figure.html.twig | 5 +- .../resources/template/body/image.html.twig | 6 +- .../src/Directives/FigureDirective.php | 19 +- 10 files changed, 532 insertions(+), 257 deletions(-) create mode 100644 Documentation-rendertest/ImagesAndFigures/FloatAndAlignment.rst create mode 100644 Documentation-rendertest/ImagesAndFigures/Zoom.rst diff --git a/Documentation-rendertest/ImagesAndFigures/FloatAndAlignment.rst b/Documentation-rendertest/ImagesAndFigures/FloatAndAlignment.rst new file mode 100644 index 000000000..8b2f07f41 --- /dev/null +++ b/Documentation-rendertest/ImagesAndFigures/FloatAndAlignment.rst @@ -0,0 +1,147 @@ +.. include:: /Includes.rst.txt + +.. _images-float-and-alignment: + +=================== +Float and alignment +=================== + +.. contents:: This page + :backlinks: top + :class: compact-list + :depth: 99 + :local: + + +.. _images-float-image-left: + +Image float left +================ + +.. |example-teaser-left| image:: ../images/q150_cccccc.png + :alt: Left floating image + :class: float-left with-shadow + +|example-teaser-left| +Typesetting is the composition of text by means of arranging physical +types[1] or the digital equivalents. Stored letters and other symbols +(called sorts in mechanical systems and glyphs in digital systems) +are retrieved and ordered according to a language's orthography for +visual display. + +.. rst-class:: clear-both + +Floating cleared. Below the image. + + +.. _images-float-image-right: + +Image float right +================= + +.. |example-teaser-right| image:: ../images/q150_cccccc.png + :alt: Right floating image + :class: float-right with-shadow + +|example-teaser-right| +Typesetting is the composition of text by means of arranging physical +types[1] or the digital equivalents. Stored letters and other symbols +(called sorts in mechanical systems and glyphs in digital systems) +are retrieved and ordered according to a language's orthography for +visual display. + +.. rst-class:: clear-both + +Floating cleared. Below the image. + + +.. _images-float-figure-left: + +Figure float left +================= + +.. figure:: ../images/q150_cccccc.png + :alt: Left floating figure + :class: float-left with-shadow + + A figure floated to the left + +Typesetting is the composition of text by means of arranging physical +types[1] or the digital equivalents. Stored letters and other symbols +(called sorts in mechanical systems and glyphs in digital systems) +are retrieved and ordered according to a language's orthography for +visual display. Typesetting requires one or more fonts. The source +could be a manuscript, a previously composed version, or a plain text. + +.. rst-class:: clear-both + +Floating cleared. Below the figure. + + +.. _images-float-figure-right: + +Figure float right +================== + +.. figure:: ../images/q150_cccccc.png + :alt: Right floating figure + :class: float-right with-shadow + + A figure floated to the right + +Typesetting is the composition of text by means of arranging physical +types[1] or the digital equivalents. Stored letters and other symbols +(called sorts in mechanical systems and glyphs in digital systems) +are retrieved and ordered according to a language's orthography for +visual display. Typesetting requires one or more fonts. The source +could be a manuscript, a previously composed version, or a plain text. + +.. rst-class:: clear-both + +Floating cleared. Below the figure. + + +.. _images-float-align-left: + +Figure align left +================= + +.. figure:: ../images/q150_cccccc.png + :alt: Left aligned figure + :align: left + + A figure aligned left via :align: + +Typesetting is the composition of text by means of arranging physical +types[1] or the digital equivalents. Stored letters and other symbols +(called sorts in mechanical systems and glyphs in digital systems) +are retrieved and ordered according to a language's orthography for +visual display. Typesetting requires one or more fonts. The source +could be a manuscript, a previously composed version, or a plain text. + +.. rst-class:: clear-both + +Floating cleared. Below the figure. + + +.. _images-float-align-right: + +Figure align right +================== + +.. figure:: ../images/q150_cccccc.png + :alt: Right aligned figure + :align: right + + A figure aligned right via :align: + +Typesetting is the composition of text by means of arranging physical +types[1] or the digital equivalents. Stored letters and other symbols +(called sorts in mechanical systems and glyphs in digital systems) +are retrieved and ordered according to a language's orthography for +visual display. Typesetting requires one or more fonts. The source +could be a manuscript, a previously composed version, or a plain text. + +.. rst-class:: clear-both + +Floating cleared. Below the figure. diff --git a/Documentation-rendertest/ImagesAndFigures/Index.rst b/Documentation-rendertest/ImagesAndFigures/Index.rst index 3e36fd32e..1c7f4b837 100644 --- a/Documentation-rendertest/ImagesAndFigures/Index.rst +++ b/Documentation-rendertest/ImagesAndFigures/Index.rst @@ -6,237 +6,11 @@ Images and figures ================== -.. contents:: This page - :backlinks: top - :class: compact-list - :depth: 99 - :local: - - -Zoom: Lightbox -============== - -**Click to enlarge.** Opens the image in a modal dialog overlay. Click anywhere -or press Escape to close. The zoom indicator (magnifying glass icon) appears at -the bottom-right corner of the image. - -.. list-table:: - :header-rows: 1 - :widths: 25 25 25 25 - - * - Plain - - Border - - Shadow - - Border + Shadow - * - .. figure:: ../images/q150_f8f8f8.png - :zoom: lightbox - - Figure - - .. figure:: ../images/q150_eeeeee.png - :zoom: lightbox - :class: with-border - - Figure + border - - .. figure:: ../images/q150_dddddd.png - :zoom: lightbox - :class: with-shadow - - Figure + shadow - - .. figure:: ../images/q150_cccccc.png - :zoom: lightbox - :class: with-border with-shadow - - Figure + both - * - .. image:: ../images/q150_f8f8f8.png - :zoom: lightbox - :alt: Image plain - - .. image:: ../images/q150_eeeeee.png - :zoom: lightbox - :alt: Image with border - :class: with-border - - .. image:: ../images/q150_dddddd.png - :zoom: lightbox - :alt: Image with shadow - :class: with-shadow - - .. image:: ../images/q150_cccccc.png - :zoom: lightbox - :alt: Image with border and shadow - :class: with-border with-shadow - - -Zoom: Gallery -============= - -**Click to open gallery.** Opens in a gallery viewer with navigation between -images in the same gallery group. Use mouse wheel to zoom, drag to pan when -zoomed. Arrow keys or buttons navigate between images. Images are grouped by -the ``:gallery:`` option value. - -Gallery Group A (screenshots): - -.. list-table:: - :widths: 33 33 33 - - * - .. figure:: ../images/q150_f8f8f8.png - :zoom: gallery - :gallery: screenshots - :class: with-border with-shadow - - Screenshot 1 - - .. figure:: ../images/q150_eeeeee.png - :zoom: gallery - :gallery: screenshots - :class: with-border with-shadow - - Screenshot 2 - - .. figure:: ../images/q150_dddddd.png - :zoom: gallery - :gallery: screenshots - :class: with-border with-shadow - - Screenshot 3 - -Gallery Group B (photos): - -.. list-table:: - :widths: 50 50 - - * - .. image:: ../images/q150_cccccc.png - :zoom: gallery - :gallery: photos - :alt: Photo 1 - :class: with-border with-shadow - - .. image:: ../images/q150_ffffff.png - :zoom: gallery - :gallery: photos - :alt: Photo 2 - :class: with-border with-shadow - - -Zoom: Lens -========== - -**Hover to magnify.** A magnifying lens follows your cursor over the image, -showing a zoomed view. The lens only activates when hovering the actual image -content (not border/padding). A result panel shows the magnified area. Use -keyboard (Enter to toggle, arrows to move) for accessibility. - -.. list-table:: - :header-rows: 1 - :widths: 25 25 25 25 - - * - Plain - - Border - - Shadow - - Border + Shadow - * - .. figure:: ../images/q150_f8f8f8.png - :zoom: lens - - Figure - - .. figure:: ../images/q150_eeeeee.png - :zoom: lens - :class: with-border - - Figure + border - - .. figure:: ../images/q150_dddddd.png - :zoom: lens - :class: with-shadow - - Figure + shadow - - .. figure:: ../images/q150_cccccc.png - :zoom: lens - :class: with-border with-shadow - - Figure + both - * - .. image:: ../images/q150_f8f8f8.png - :zoom: lens - :alt: Image plain - - .. image:: ../images/q150_eeeeee.png - :zoom: lens - :alt: Image with border - :class: with-border - - .. image:: ../images/q150_dddddd.png - :zoom: lens - :alt: Image with shadow - :class: with-shadow - - .. image:: ../images/q150_cccccc.png - :zoom: lens - :alt: Image with border and shadow - :class: with-border with-shadow - - -Zoom: Inline -============ - -**Scroll to zoom.** Use mouse wheel directly on the image to zoom in/out. -When zoomed, drag to pan. Double-click or press Escape to reset. The expand -icon indicates scroll zoom is available. Keyboard: +/- to zoom, arrows to pan. - -.. list-table:: - :header-rows: 1 - :widths: 25 25 25 25 - - * - Plain - - Border - - Shadow - - Border + Shadow - * - .. figure:: ../images/q150_f8f8f8.png - :zoom: inline - - Figure - - .. figure:: ../images/q150_eeeeee.png - :zoom: inline - :class: with-border - - Figure + border - - .. figure:: ../images/q150_dddddd.png - :zoom: inline - :class: with-shadow - - Figure + shadow - - .. figure:: ../images/q150_cccccc.png - :zoom: inline - :class: with-border with-shadow - - Figure + both - * - .. image:: ../images/q150_f8f8f8.png - :zoom: inline - :alt: Image plain - - .. image:: ../images/q150_eeeeee.png - :zoom: inline - :alt: Image with border - :class: with-border - - .. image:: ../images/q150_dddddd.png - :zoom: inline - :alt: Image with shadow - :class: with-shadow - - .. image:: ../images/q150_cccccc.png - :zoom: inline - :alt: Image with border and shadow - :class: with-border with-shadow - - -Zoom Indicator Options -====================== - -The zoom indicator can be hidden using ``:zoom-indicator: false``: - -.. list-table:: - :widths: 50 50 +.. toctree:: + :titlesonly: - * - With indicator (default) - - Without indicator - * - .. figure:: ../images/q150_f8f8f8.png - :zoom: lightbox - :class: with-border with-shadow - - Indicator visible - - .. figure:: ../images/q150_eeeeee.png - :zoom: lightbox - :zoom-indicator: false - :class: with-border with-shadow - - Indicator hidden + Zoom + FloatAndAlignment Bright images with border and shadow @@ -311,25 +85,6 @@ Bright images as figures with caption Image with border and shadow and background color #f8f8f8 -Image float left -================ - -.. |example-teaser-left| image:: ../images/q150_cccccc.png - :alt: Left floating image - :class: float-left with-shadow - -|example-teaser-left| -Typesetting is the composition of text by means of arranging physical -types[1] or the digital equivalents. Stored letters and other symbols -(called sorts in mechanical systems and glyphs in digital systems) -are retrieved and ordered according to a language's orthography for -visual display. - -.. rst-class:: clear-both - -Floating cleared. Below the image. - - Images and Admonitions ====================== diff --git a/Documentation-rendertest/ImagesAndFigures/Zoom.rst b/Documentation-rendertest/ImagesAndFigures/Zoom.rst new file mode 100644 index 000000000..c46dca1d5 --- /dev/null +++ b/Documentation-rendertest/ImagesAndFigures/Zoom.rst @@ -0,0 +1,266 @@ +.. include:: /Includes.rst.txt + +.. _images-zoom: + +==== +Zoom +==== + +.. contents:: This page + :backlinks: top + :class: compact-list + :depth: 99 + :local: + + +.. _images-zoom-lightbox: + +Zoom: Lightbox +============== + +**Click to enlarge.** Opens the image in a modal dialog overlay. Click anywhere +or press Escape to close. The zoom indicator (magnifying glass icon) appears at +the bottom-right corner of the image. + +.. list-table:: + :header-rows: 1 + :widths: 25 25 25 25 + + * - Plain + - Border + - Shadow + - Border + Shadow + * - .. figure:: ../images/q150_f8f8f8.png + :alt: Figure with lightbox zoom + :zoom: lightbox + + Figure + - .. figure:: ../images/q150_eeeeee.png + :alt: Figure with lightbox zoom and border + :zoom: lightbox + :class: with-border + + Figure + border + - .. figure:: ../images/q150_dddddd.png + :alt: Figure with lightbox zoom and shadow + :zoom: lightbox + :class: with-shadow + + Figure + shadow + - .. figure:: ../images/q150_cccccc.png + :alt: Figure with lightbox zoom, border and shadow + :zoom: lightbox + :class: with-border with-shadow + + Figure + both + * - .. image:: ../images/q150_f8f8f8.png + :zoom: lightbox + :alt: Image plain + - .. image:: ../images/q150_eeeeee.png + :zoom: lightbox + :alt: Image with border + :class: with-border + - .. image:: ../images/q150_dddddd.png + :zoom: lightbox + :alt: Image with shadow + :class: with-shadow + - .. image:: ../images/q150_cccccc.png + :zoom: lightbox + :alt: Image with border and shadow + :class: with-border with-shadow + + +.. _images-zoom-gallery: + +Zoom: Gallery +============= + +**Click to open gallery.** Opens in a gallery viewer with navigation between +images in the same gallery group. Use mouse wheel to zoom, drag to pan when +zoomed. Arrow keys or buttons navigate between images. Images are grouped by +the ``:gallery:`` option value. + +Gallery Group A (screenshots): + +.. list-table:: + :widths: 33 33 33 + + * - .. figure:: ../images/q150_f8f8f8.png + :alt: Screenshot 1 in gallery group + :zoom: gallery + :gallery: screenshots + :class: with-border with-shadow + + Screenshot 1 + - .. figure:: ../images/q150_eeeeee.png + :alt: Screenshot 2 in gallery group + :zoom: gallery + :gallery: screenshots + :class: with-border with-shadow + + Screenshot 2 + - .. figure:: ../images/q150_dddddd.png + :alt: Screenshot 3 in gallery group + :zoom: gallery + :gallery: screenshots + :class: with-border with-shadow + + Screenshot 3 + +Gallery Group B (photos): + +.. list-table:: + :widths: 50 50 + + * - .. image:: ../images/q150_cccccc.png + :zoom: gallery + :gallery: photos + :alt: Photo 1 + :class: with-border with-shadow + - .. image:: ../images/q150_ffffff.png + :zoom: gallery + :gallery: photos + :alt: Photo 2 + :class: with-border with-shadow + + +.. _images-zoom-lens: + +Zoom: Lens +========== + +**Hover to magnify.** A magnifying lens follows your cursor over the image, +showing a zoomed view. The lens only activates when hovering the actual image +content (not border/padding). A result panel shows the magnified area. Use +keyboard (Enter to toggle, arrows to move) for accessibility. + +.. list-table:: + :header-rows: 1 + :widths: 25 25 25 25 + + * - Plain + - Border + - Shadow + - Border + Shadow + * - .. figure:: ../images/q150_f8f8f8.png + :alt: Figure with lens zoom + :zoom: lens + + Figure + - .. figure:: ../images/q150_eeeeee.png + :alt: Figure with lens zoom and border + :zoom: lens + :class: with-border + + Figure + border + - .. figure:: ../images/q150_dddddd.png + :alt: Figure with lens zoom and shadow + :zoom: lens + :class: with-shadow + + Figure + shadow + - .. figure:: ../images/q150_cccccc.png + :alt: Figure with lens zoom, border and shadow + :zoom: lens + :class: with-border with-shadow + + Figure + both + * - .. image:: ../images/q150_f8f8f8.png + :zoom: lens + :alt: Image plain + - .. image:: ../images/q150_eeeeee.png + :zoom: lens + :alt: Image with border + :class: with-border + - .. image:: ../images/q150_dddddd.png + :zoom: lens + :alt: Image with shadow + :class: with-shadow + - .. image:: ../images/q150_cccccc.png + :zoom: lens + :alt: Image with border and shadow + :class: with-border with-shadow + + +.. _images-zoom-inline: + +Zoom: Inline +============ + +**Scroll to zoom.** Use mouse wheel directly on the image to zoom in/out. +When zoomed, drag to pan. Double-click or press Escape to reset. The expand +icon indicates scroll zoom is available. Keyboard: +/- to zoom, arrows to pan. + +.. list-table:: + :header-rows: 1 + :widths: 25 25 25 25 + + * - Plain + - Border + - Shadow + - Border + Shadow + * - .. figure:: ../images/q150_f8f8f8.png + :alt: Figure with inline zoom + :zoom: inline + + Figure + - .. figure:: ../images/q150_eeeeee.png + :alt: Figure with inline zoom and border + :zoom: inline + :class: with-border + + Figure + border + - .. figure:: ../images/q150_dddddd.png + :alt: Figure with inline zoom and shadow + :zoom: inline + :class: with-shadow + + Figure + shadow + - .. figure:: ../images/q150_cccccc.png + :alt: Figure with inline zoom, border and shadow + :zoom: inline + :class: with-border with-shadow + + Figure + both + * - .. image:: ../images/q150_f8f8f8.png + :zoom: inline + :alt: Image plain + - .. image:: ../images/q150_eeeeee.png + :zoom: inline + :alt: Image with border + :class: with-border + - .. image:: ../images/q150_dddddd.png + :zoom: inline + :alt: Image with shadow + :class: with-shadow + - .. image:: ../images/q150_cccccc.png + :zoom: inline + :alt: Image with border and shadow + :class: with-border with-shadow + + +.. _images-zoom-indicator-options: + +Zoom indicator options +====================== + +The zoom indicator can be hidden using ``:zoom-indicator: false``: + +.. list-table:: + :widths: 50 50 + + * - With indicator (default) + - Without indicator + * - .. figure:: ../images/q150_f8f8f8.png + :alt: Figure with zoom indicator visible + :zoom: lightbox + :class: with-border with-shadow + + Indicator visible + - .. figure:: ../images/q150_eeeeee.png + :alt: Figure with zoom indicator hidden + :zoom: lightbox + :zoom-indicator: false + :class: with-border with-shadow + + Indicator hidden diff --git a/packages/typo3-docs-theme/assets/sass/_utilities.scss b/packages/typo3-docs-theme/assets/sass/_utilities.scss index f3b60fde1..8b6293ef0 100644 --- a/packages/typo3-docs-theme/assets/sass/_utilities.scss +++ b/packages/typo3-docs-theme/assets/sass/_utilities.scss @@ -11,7 +11,8 @@ .no-focus { outline: none !important; } -.cc { +.cc, +.clear-both { clear: both; } .rubric { diff --git a/packages/typo3-docs-theme/assets/sass/components/_images.scss b/packages/typo3-docs-theme/assets/sass/components/_images.scss index d23c53949..5c637cdfd 100644 --- a/packages/typo3-docs-theme/assets/sass/components/_images.scss +++ b/packages/typo3-docs-theme/assets/sass/components/_images.scss @@ -17,4 +17,31 @@ figure { &:not(:first-child) { @extend .mt-3; } + &.float-left, + &.float-right { + margin-bottom: $spacer; + max-width: 50%; + } + &.float-left { + float: left; + margin-right: $spacer; + } + &.float-right { + float: right; + margin-left: $spacer; + } + &.align-center { + margin-left: auto; + margin-right: auto; + } +} + +@media (max-width: 575.98px) { + figure.float-left, + figure.float-right { + float: none; + max-width: 100%; + margin-left: 0; + margin-right: 0; + } } diff --git a/packages/typo3-docs-theme/assets/sass/components/_rst.scss b/packages/typo3-docs-theme/assets/sass/components/_rst.scss index 481a69edd..daef8b2d7 100644 --- a/packages/typo3-docs-theme/assets/sass/components/_rst.scss +++ b/packages/typo3-docs-theme/assets/sass/components/_rst.scss @@ -33,13 +33,29 @@ max-width: 100%; height: auto; &.float-left { + float: left; margin-right: $spacer; margin-bottom: $spacer; } &.float-right { + float: right; margin-left: $spacer; margin-bottom: $spacer; } + &.align-center { + display: block; + margin-left: auto; + margin-right: auto; + } + @media (max-width: 575.98px) { + &.float-left, + &.float-right { + float: none; + margin-left: 0; + margin-right: 0; + max-width: 100%; + } + } } .plantuml object { height: auto !important; diff --git a/packages/typo3-docs-theme/resources/public/css/theme.css b/packages/typo3-docs-theme/resources/public/css/theme.css index 2069bf916..974fc1e7f 100644 --- a/packages/typo3-docs-theme/resources/public/css/theme.css +++ b/packages/typo3-docs-theme/resources/public/css/theme.css @@ -24700,6 +24700,32 @@ dl.command .command-arguments section { figure figcaption p:last-child { margin-bottom: 0; } +figure.float-left, figure.float-right { + margin-bottom: 1rem; + max-width: 50%; +} +figure.float-left { + float: left; + margin-right: 1rem; +} +figure.float-right { + float: right; + margin-left: 1rem; +} +figure.align-center { + margin-left: auto; + margin-right: auto; +} + +@media (max-width: 575.98px) { + figure.float-left, + figure.float-right { + float: none; + max-width: 100%; + margin-left: 0; + margin-right: 0; + } +} span[id*=MathJax-Span] { color: #333333; } @@ -24797,14 +24823,32 @@ article *:hover > a.headerlink, article *:hover > a.permalink, article *:hover . } .rst-content img.float-left, .rst-content object.float-left { + float: left; margin-right: 1rem; margin-bottom: 1rem; } .rst-content img.float-right, .rst-content object.float-right { + float: right; margin-left: 1rem; margin-bottom: 1rem; } +.rst-content img.align-center, +.rst-content object.align-center { + display: block; + margin-left: auto; + margin-right: auto; +} +@media (max-width: 575.98px) { + .rst-content img.float-left, .rst-content img.float-right, + .rst-content object.float-left, + .rst-content object.float-right { + float: none; + margin-left: 0; + margin-right: 0; + max-width: 100%; + } +} .rst-content .plantuml object { height: auto !important; } @@ -26735,7 +26779,8 @@ section.confval-section { outline: none !important; } -.cc { +.cc, +.clear-both { clear: both; } diff --git a/packages/typo3-docs-theme/resources/template/body/figure.html.twig b/packages/typo3-docs-theme/resources/template/body/figure.html.twig index 916d3ead1..9a46ae8ff 100644 --- a/packages/typo3-docs-theme/resources/template/body/figure.html.twig +++ b/packages/typo3-docs-theme/resources/template/body/figure.html.twig @@ -1,5 +1,8 @@ +{%- set alignMap = {'left': 'float-left', 'right': 'float-right', 'center': 'align-center'} -%} +{%- set alignClass = node.hasOption('align') ? alignMap[node.option('align')]|default('') : '' -%} +{%- set classes = ([node.classesString, alignClass]|join(' '))|trim -%}
{%- endif -%} +{%- set alignMap = {'left': 'float-left', 'right': 'float-right', 'center': 'align-center'} -%} +{%- set alignClass = node.hasOption('align') ? alignMap[node.option('align')]|default('') : '' -%} +{%- set classes = ([node.classesString, alignClass]|join(' '))|trim -%} {%- if node.hasOption('zoom') -%} diff --git a/packages/typo3-docs-theme/src/Directives/FigureDirective.php b/packages/typo3-docs-theme/src/Directives/FigureDirective.php index 3e2d45fba..83e367c4b 100644 --- a/packages/typo3-docs-theme/src/Directives/FigureDirective.php +++ b/packages/typo3-docs-theme/src/Directives/FigureDirective.php @@ -25,6 +25,9 @@ use function dirname; use function in_array; +use function is_string; +use function preg_replace; +use function trim; /** * Renders a figure with optional zoom functionality. @@ -82,15 +85,26 @@ public function process( dirname($blockContext->getDocumentParserContext()->getContext()->getCurrentAbsolutePath()), $directive->getData(), )); + // Strip float classes from the inner image - floating should only + // apply to the
element to keep the caption below the image. + // Note: Currently the framework's postProcessNode() only processes the + // top-level FigureNode, so the inner image's classesString is empty. + // We strip here for forward-compatibility should this behavior change. + $imageClass = isset($scalarOptions['class']) && is_string($scalarOptions['class']) + ? $scalarOptions['class'] + : null; + if ($imageClass !== null) { + $imageClass = trim((string) preg_replace('/\bfloat-(left|right)\b/', '', $imageClass)) ?: null; + } + $image = $image->withOptions([ 'width' => $scalarOptions['width'] ?? null, 'height' => $scalarOptions['height'] ?? null, 'alt' => $scalarOptions['alt'] ?? null, 'scale' => $scalarOptions['scale'] ?? null, 'target' => $scalarOptions['target'] ?? null, - 'class' => $scalarOptions['class'] ?? null, + 'class' => $imageClass, 'name' => $scalarOptions['name'] ?? null, - 'align' => $scalarOptions['align'] ?? null, ]); $figureNode = new FigureNode($image, new CollectionNode($collectionNode->getChildren())); @@ -116,7 +130,6 @@ public function process( $filteredOptions['target'], $filteredOptions['class'], $filteredOptions['name'], - $filteredOptions['align'], ); if (!empty($filteredOptions)) { From 9c486516a7eddd31f9fd422e2d12c0908635a1e2 Mon Sep 17 00:00:00 2001 From: Sebastian Mendel Date: Fri, 20 Feb 2026 14:07:07 +0100 Subject: [PATCH 2/2] test: add integration tests for float and alignment Cover all float/alignment scenarios: - Figure :align: left/right/center maps to CSS classes - Image :align: left/right/center maps to CSS classes - Figure :class: float-left/right (float stripped from inner img) - Mixed :class: float-left with-shadow (extra classes preserved) - Image :class: float-right - Combined :align: + :class: (both applied to figure element) --- .../tests/images/Float/expected/index.html | 123 ++++++++++++++++++ .../tests/images/Float/input/guides.xml | 7 + .../tests/images/Float/input/index.rst | 97 ++++++++++++++ .../tests/images/Float/input/typo3-logo.png | Bin 0 -> 12708 bytes 4 files changed, 227 insertions(+) create mode 100644 tests/Integration/tests/images/Float/expected/index.html create mode 100644 tests/Integration/tests/images/Float/input/guides.xml create mode 100644 tests/Integration/tests/images/Float/input/index.rst create mode 100644 tests/Integration/tests/images/Float/input/typo3-logo.png diff --git a/tests/Integration/tests/images/Float/expected/index.html b/tests/Integration/tests/images/Float/expected/index.html new file mode 100644 index 000000000..ae08a9f23 --- /dev/null +++ b/tests/Integration/tests/images/Float/expected/index.html @@ -0,0 +1,123 @@ + +
+

Document Title 

+ +

Lorem Ipsum Dolor.

+ +
+

Figure align left 

+
+ Left aligned figure + + +
+

Caption for left-aligned figure

+
+
+
+
+

Figure align right 

+
+ Right aligned figure + + +
+

Caption for right-aligned figure

+
+
+
+
+

Figure align center 

+
+ Center aligned figure + + +
+

Caption for center-aligned figure

+
+
+
+
+

Image align left 

+ Left aligned image +
+
+

Image align right 

+ Right aligned image +
+
+

Image align center 

+ Center aligned image +
+
+

Figure with float class 

+
+ Figure with float-left class + + +
+

Float class on figure

+
+
+
+
+

Figure with float-right class 

+
+ Figure with float-right class + + +
+

Float-right class on figure

+
+
+
+
+

Figure with float class and extra classes 

+
+ Figure with float and shadow + + +
+

Float class combined with other classes

+
+
+
+
+

Image with float class 

+ Image with float-right class +
+
+

Figure with align and extra class 

+
+ Figure with align and border + + +
+

Align option combined with extra class

+
+
+
+
+ diff --git a/tests/Integration/tests/images/Float/input/guides.xml b/tests/Integration/tests/images/Float/input/guides.xml new file mode 100644 index 000000000..6101db50c --- /dev/null +++ b/tests/Integration/tests/images/Float/input/guides.xml @@ -0,0 +1,7 @@ + + + diff --git a/tests/Integration/tests/images/Float/input/index.rst b/tests/Integration/tests/images/Float/input/index.rst new file mode 100644 index 000000000..6a8a4d3c6 --- /dev/null +++ b/tests/Integration/tests/images/Float/input/index.rst @@ -0,0 +1,97 @@ +============== +Document Title +============== + +Lorem Ipsum Dolor. + +Figure align left +================= + +.. figure:: /typo3-logo.png + :alt: Left aligned figure + :align: left + + Caption for left-aligned figure + +Figure align right +================== + +.. figure:: /typo3-logo.png + :alt: Right aligned figure + :align: right + + Caption for right-aligned figure + +Figure align center +=================== + +.. figure:: /typo3-logo.png + :alt: Center aligned figure + :align: center + + Caption for center-aligned figure + +Image align left +================ + +.. image:: /typo3-logo.png + :alt: Left aligned image + :align: left + +Image align right +================= + +.. image:: /typo3-logo.png + :alt: Right aligned image + :align: right + +Image align center +================== + +.. image:: /typo3-logo.png + :alt: Center aligned image + :align: center + +Figure with float class +======================= + +.. figure:: /typo3-logo.png + :alt: Figure with float-left class + :class: float-left + + Float class on figure + +Figure with float-right class +============================= + +.. figure:: /typo3-logo.png + :alt: Figure with float-right class + :class: float-right + + Float-right class on figure + +Figure with float class and extra classes +========================================= + +.. figure:: /typo3-logo.png + :alt: Figure with float and shadow + :class: float-left with-shadow + + Float class combined with other classes + +Image with float class +====================== + +.. image:: /typo3-logo.png + :alt: Image with float-right class + :class: float-right + +Figure with align and extra class +================================= + +.. figure:: /typo3-logo.png + :alt: Figure with align and border + :align: left + :class: with-border + + Align option combined with extra class diff --git a/tests/Integration/tests/images/Float/input/typo3-logo.png b/tests/Integration/tests/images/Float/input/typo3-logo.png new file mode 100644 index 0000000000000000000000000000000000000000..a89ae91c37473c58675e7ff8557baff11a05512f GIT binary patch literal 12708 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 literal 0 HcmV?d00001