@wordpress/block-library 8.25.0 → 8.25.1-next.79a6196f.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (250) hide show
  1. package/LICENSE.md +1 -1
  2. package/build/audio/edit.js +10 -12
  3. package/build/audio/edit.js.map +1 -1
  4. package/build/block/edit.js +81 -34
  5. package/build/block/edit.js.map +1 -1
  6. package/build/comments-title/deprecated.js +1 -1
  7. package/build/comments-title/index.js +1 -1
  8. package/build/cover/edit/inspector-controls.js +1 -1
  9. package/build/cover/edit/inspector-controls.js.map +1 -1
  10. package/build/cover/edit.native.js +1 -0
  11. package/build/cover/edit.native.js.map +1 -1
  12. package/build/embed/util.js +4 -4
  13. package/build/embed/util.js.map +1 -1
  14. package/build/file/edit.js +19 -27
  15. package/build/file/edit.js.map +1 -1
  16. package/build/gallery/edit.js +36 -17
  17. package/build/gallery/edit.js.map +1 -1
  18. package/build/gallery/gallery.js +3 -2
  19. package/build/gallery/gallery.js.map +1 -1
  20. package/build/gallery/index.js +4 -0
  21. package/build/gallery/index.js.map +1 -1
  22. package/build/gallery/transforms.js +4 -68
  23. package/build/gallery/transforms.js.map +1 -1
  24. package/build/group/index.js +5 -1
  25. package/build/group/index.js.map +1 -1
  26. package/build/image/deprecated.js +11 -0
  27. package/build/image/deprecated.js.map +1 -1
  28. package/build/image/edit.native.js +3 -0
  29. package/build/image/edit.native.js.map +1 -1
  30. package/build/image/image.js +22 -21
  31. package/build/image/image.js.map +1 -1
  32. package/build/image/view.js +7 -1
  33. package/build/image/view.js.map +1 -1
  34. package/build/list/edit.js +10 -15
  35. package/build/list/edit.js.map +1 -1
  36. package/build/list-item/edit.js +17 -2
  37. package/build/list-item/edit.js.map +1 -1
  38. package/build/list-item/hooks/use-enter.js +5 -3
  39. package/build/list-item/hooks/use-enter.js.map +1 -1
  40. package/build/list-item/hooks/use-enter.native.js +4 -3
  41. package/build/list-item/hooks/use-enter.native.js.map +1 -1
  42. package/build/list-item/hooks/use-indent-list-item.js +2 -3
  43. package/build/list-item/hooks/use-indent-list-item.js.map +1 -1
  44. package/build/list-item/hooks/use-merge.js +1 -1
  45. package/build/list-item/hooks/use-merge.js.map +1 -1
  46. package/build/list-item/hooks/use-outdent-list-item.js +3 -17
  47. package/build/list-item/hooks/use-outdent-list-item.js.map +1 -1
  48. package/build/list-item/hooks/use-space.js +8 -4
  49. package/build/list-item/hooks/use-space.js.map +1 -1
  50. package/build/media-text/media-container.native.js +3 -0
  51. package/build/media-text/media-container.native.js.map +1 -1
  52. package/build/navigation/constants.js +3 -1
  53. package/build/navigation/constants.js.map +1 -1
  54. package/build/navigation/edit/index.js +4 -0
  55. package/build/navigation/edit/index.js.map +1 -1
  56. package/build/navigation/view.js +25 -1
  57. package/build/navigation/view.js.map +1 -1
  58. package/build/paragraph/index.js +0 -1
  59. package/build/paragraph/index.js.map +1 -1
  60. package/build/paragraph/transforms.js +0 -1
  61. package/build/paragraph/transforms.js.map +1 -1
  62. package/build/pattern/edit.js +24 -2
  63. package/build/pattern/edit.js.map +1 -1
  64. package/build/pattern/recursion-detector.js +147 -0
  65. package/build/pattern/recursion-detector.js.map +1 -0
  66. package/build/post-featured-image/edit.js +19 -2
  67. package/build/post-featured-image/edit.js.map +1 -1
  68. package/build/post-featured-image/index.js +4 -0
  69. package/build/post-featured-image/index.js.map +1 -1
  70. package/build/query/edit/inspector-controls/index.js +3 -1
  71. package/build/query/edit/inspector-controls/index.js.map +1 -1
  72. package/build/query-pagination-numbers/index.js +1 -1
  73. package/build/search/edit.js +3 -5
  74. package/build/search/edit.js.map +1 -1
  75. package/build/search/index.js +0 -4
  76. package/build/search/index.js.map +1 -1
  77. package/build/site-logo/edit.js +7 -6
  78. package/build/site-logo/edit.js.map +1 -1
  79. package/build/spacer/edit.native.js +2 -2
  80. package/build/spacer/edit.native.js.map +1 -1
  81. package/build/tag-cloud/edit.js +5 -9
  82. package/build/tag-cloud/edit.js.map +1 -1
  83. package/build/utils/constants.js +16 -0
  84. package/build/utils/constants.js.map +1 -0
  85. package/build/video/edit.js +11 -8
  86. package/build/video/edit.js.map +1 -1
  87. package/build-module/audio/edit.js +10 -12
  88. package/build-module/audio/edit.js.map +1 -1
  89. package/build-module/block/edit.js +85 -38
  90. package/build-module/block/edit.js.map +1 -1
  91. package/build-module/comments-title/deprecated.js +1 -1
  92. package/build-module/comments-title/index.js +1 -1
  93. package/build-module/cover/edit/inspector-controls.js +1 -1
  94. package/build-module/cover/edit/inspector-controls.js.map +1 -1
  95. package/build-module/cover/edit.native.js +1 -0
  96. package/build-module/cover/edit.native.js.map +1 -1
  97. package/build-module/embed/util.js +4 -4
  98. package/build-module/embed/util.js.map +1 -1
  99. package/build-module/file/edit.js +19 -27
  100. package/build-module/file/edit.js.map +1 -1
  101. package/build-module/gallery/edit.js +36 -17
  102. package/build-module/gallery/edit.js.map +1 -1
  103. package/build-module/gallery/gallery.js +3 -2
  104. package/build-module/gallery/gallery.js.map +1 -1
  105. package/build-module/gallery/index.js +4 -0
  106. package/build-module/gallery/index.js.map +1 -1
  107. package/build-module/gallery/transforms.js +4 -68
  108. package/build-module/gallery/transforms.js.map +1 -1
  109. package/build-module/group/index.js +5 -1
  110. package/build-module/group/index.js.map +1 -1
  111. package/build-module/image/deprecated.js +11 -0
  112. package/build-module/image/deprecated.js.map +1 -1
  113. package/build-module/image/edit.native.js +3 -0
  114. package/build-module/image/edit.native.js.map +1 -1
  115. package/build-module/image/image.js +17 -16
  116. package/build-module/image/image.js.map +1 -1
  117. package/build-module/image/view.js +7 -1
  118. package/build-module/image/view.js.map +1 -1
  119. package/build-module/list/edit.js +10 -15
  120. package/build-module/list/edit.js.map +1 -1
  121. package/build-module/list-item/edit.js +18 -3
  122. package/build-module/list-item/edit.js.map +1 -1
  123. package/build-module/list-item/hooks/use-enter.js +5 -3
  124. package/build-module/list-item/hooks/use-enter.js.map +1 -1
  125. package/build-module/list-item/hooks/use-enter.native.js +4 -3
  126. package/build-module/list-item/hooks/use-enter.native.js.map +1 -1
  127. package/build-module/list-item/hooks/use-indent-list-item.js +2 -3
  128. package/build-module/list-item/hooks/use-indent-list-item.js.map +1 -1
  129. package/build-module/list-item/hooks/use-merge.js +1 -1
  130. package/build-module/list-item/hooks/use-merge.js.map +1 -1
  131. package/build-module/list-item/hooks/use-outdent-list-item.js +3 -17
  132. package/build-module/list-item/hooks/use-outdent-list-item.js.map +1 -1
  133. package/build-module/list-item/hooks/use-space.js +8 -4
  134. package/build-module/list-item/hooks/use-space.js.map +1 -1
  135. package/build-module/media-text/media-container.native.js +3 -0
  136. package/build-module/media-text/media-container.native.js.map +1 -1
  137. package/build-module/navigation/constants.js +1 -0
  138. package/build-module/navigation/constants.js.map +1 -1
  139. package/build-module/navigation/edit/index.js +5 -1
  140. package/build-module/navigation/edit/index.js.map +1 -1
  141. package/build-module/navigation/view.js +25 -1
  142. package/build-module/navigation/view.js.map +1 -1
  143. package/build-module/paragraph/index.js +0 -1
  144. package/build-module/paragraph/index.js.map +1 -1
  145. package/build-module/paragraph/transforms.js +0 -1
  146. package/build-module/paragraph/transforms.js.map +1 -1
  147. package/build-module/pattern/edit.js +26 -4
  148. package/build-module/pattern/edit.js.map +1 -1
  149. package/build-module/pattern/recursion-detector.js +139 -0
  150. package/build-module/pattern/recursion-detector.js.map +1 -0
  151. package/build-module/post-featured-image/edit.js +19 -2
  152. package/build-module/post-featured-image/edit.js.map +1 -1
  153. package/build-module/post-featured-image/index.js +4 -0
  154. package/build-module/post-featured-image/index.js.map +1 -1
  155. package/build-module/query/edit/inspector-controls/index.js +3 -1
  156. package/build-module/query/edit/inspector-controls/index.js.map +1 -1
  157. package/build-module/query-pagination-numbers/index.js +1 -1
  158. package/build-module/search/edit.js +3 -5
  159. package/build-module/search/edit.js.map +1 -1
  160. package/build-module/search/index.js +0 -4
  161. package/build-module/search/index.js.map +1 -1
  162. package/build-module/site-logo/edit.js +7 -6
  163. package/build-module/site-logo/edit.js.map +1 -1
  164. package/build-module/spacer/edit.native.js +2 -2
  165. package/build-module/spacer/edit.native.js.map +1 -1
  166. package/build-module/tag-cloud/edit.js +6 -10
  167. package/build-module/tag-cloud/edit.js.map +1 -1
  168. package/build-module/utils/constants.js +9 -0
  169. package/build-module/utils/constants.js.map +1 -0
  170. package/build-module/video/edit.js +11 -8
  171. package/build-module/video/edit.js.map +1 -1
  172. package/build-style/button/editor-rtl.css +0 -37
  173. package/build-style/button/editor.css +0 -37
  174. package/build-style/button/style-rtl.css +6 -6
  175. package/build-style/button/style.css +6 -6
  176. package/build-style/editor-rtl.css +2 -44
  177. package/build-style/editor.css +2 -44
  178. package/build-style/navigation/editor-rtl.css +2 -4
  179. package/build-style/navigation/editor.css +2 -4
  180. package/build-style/navigation/style-rtl.css +14 -18
  181. package/build-style/navigation/style.css +14 -18
  182. package/build-style/search/style-rtl.css +26 -27
  183. package/build-style/search/style.css +26 -27
  184. package/build-style/style-rtl.css +46 -51
  185. package/build-style/style.css +46 -51
  186. package/build-style/table/editor-rtl.css +0 -3
  187. package/build-style/table/editor.css +0 -3
  188. package/package.json +32 -32
  189. package/src/audio/edit.js +19 -19
  190. package/src/audio/test/__snapshots__/edit.native.js.snap +12 -0
  191. package/src/audio/test/edit.native.js +29 -0
  192. package/src/block/edit.js +120 -66
  193. package/src/button/editor.scss +0 -43
  194. package/src/button/style.scss +6 -6
  195. package/src/buttons/test/__snapshots__/edit.native.js.snap +6 -0
  196. package/src/buttons/test/edit.native.js +49 -0
  197. package/src/comments-title/block.json +1 -1
  198. package/src/cover/edit/inspector-controls.js +1 -1
  199. package/src/cover/edit.native.js +1 -0
  200. package/src/embed/util.js +2 -2
  201. package/src/file/edit.js +17 -24
  202. package/src/gallery/block.json +4 -0
  203. package/src/gallery/edit.js +69 -42
  204. package/src/gallery/gallery.js +4 -1
  205. package/src/gallery/index.php +15 -0
  206. package/src/gallery/transforms.js +2 -55
  207. package/src/group/block.json +5 -1
  208. package/src/image/deprecated.js +8 -0
  209. package/src/image/edit.native.js +3 -0
  210. package/src/image/image.js +54 -35
  211. package/src/image/index.php +1 -6
  212. package/src/image/view.js +5 -2
  213. package/src/list/edit.js +27 -35
  214. package/src/list-item/edit.js +18 -2
  215. package/src/list-item/hooks/use-enter.js +63 -62
  216. package/src/list-item/hooks/use-enter.native.js +9 -5
  217. package/src/list-item/hooks/use-indent-list-item.js +43 -53
  218. package/src/list-item/hooks/use-merge.js +1 -1
  219. package/src/list-item/hooks/use-outdent-list-item.js +50 -69
  220. package/src/list-item/hooks/use-space.js +7 -4
  221. package/src/media-text/media-container.native.js +3 -1
  222. package/src/navigation/constants.js +2 -0
  223. package/src/navigation/edit/index.js +11 -1
  224. package/src/navigation/editor.scss +1 -1
  225. package/src/navigation/style.scss +18 -16
  226. package/src/navigation/view.js +29 -3
  227. package/src/paragraph/block.json +0 -1
  228. package/src/paragraph/test/__snapshots__/edit.native.js.snap +12 -0
  229. package/src/paragraph/test/edit.native.js +114 -0
  230. package/src/pattern/edit.js +35 -3
  231. package/src/pattern/index.php +16 -0
  232. package/src/pattern/recursion-detector.js +145 -0
  233. package/src/pattern/test/index.js +74 -0
  234. package/src/post-featured-image/block.json +4 -0
  235. package/src/post-featured-image/edit.js +32 -1
  236. package/src/post-featured-image/index.php +31 -0
  237. package/src/query/edit/inspector-controls/index.js +2 -0
  238. package/src/query-pagination-numbers/block.json +1 -1
  239. package/src/search/block.json +0 -4
  240. package/src/search/edit.js +2 -8
  241. package/src/search/index.php +3 -7
  242. package/src/search/style.scss +27 -29
  243. package/src/site-logo/edit.js +3 -4
  244. package/src/social-link/index.php +1 -1
  245. package/src/spacer/edit.native.js +4 -2
  246. package/src/table/editor.scss +0 -3
  247. package/src/tag-cloud/edit.js +7 -7
  248. package/src/template-part/index.php +6 -0
  249. package/src/utils/constants.js +8 -0
  250. package/src/video/edit.js +29 -27
@@ -4,6 +4,7 @@
4
4
  import {
5
5
  act,
6
6
  addBlock,
7
+ dismissModal,
7
8
  getBlock,
8
9
  typeInRichText,
9
10
  fireEvent,
@@ -15,6 +16,7 @@ import {
15
16
  within,
16
17
  withFakeTimers,
17
18
  waitForElementToBeRemoved,
19
+ waitForModalVisible,
18
20
  } from 'test/helpers';
19
21
  import Clipboard from '@react-native-clipboard/clipboard';
20
22
  import TextInputState from 'react-native/Libraries/Components/TextInput/TextInputState';
@@ -687,6 +689,118 @@ describe( 'Paragraph block', () => {
687
689
  ` );
688
690
  } );
689
691
 
692
+ it( 'should show the expected font sizes values', async () => {
693
+ // Arrange
694
+ const screen = await initializeEditor( { withGlobalStyles: true } );
695
+ await addBlock( screen, 'Paragraph' );
696
+
697
+ // Act
698
+ const paragraphBlock = getBlock( screen, 'Paragraph' );
699
+ fireEvent.press( paragraphBlock );
700
+ const paragraphTextInput =
701
+ within( paragraphBlock ).getByPlaceholderText( 'Start writing…' );
702
+ typeInRichText(
703
+ paragraphTextInput,
704
+ 'A quick brown fox jumps over the lazy dog.'
705
+ );
706
+ // Open Block Settings.
707
+ fireEvent.press( screen.getByLabelText( 'Open Settings' ) );
708
+
709
+ // Wait for Block Settings to be visible.
710
+ const blockSettingsModal = screen.getByTestId( 'block-settings-modal' );
711
+ await waitForModalVisible( blockSettingsModal );
712
+
713
+ // Open Font size settings
714
+ fireEvent.press( screen.getByLabelText( 'Font Size, Custom' ) );
715
+ await waitFor( () => screen.getByLabelText( 'Selected: Default' ) );
716
+
717
+ // Assert
718
+ const modalContent = within( blockSettingsModal );
719
+ expect( modalContent.getByLabelText( 'Small' ) ).toBeVisible();
720
+ expect( modalContent.getByText( '14px' ) ).toBeVisible();
721
+ expect( modalContent.getByLabelText( 'Medium' ) ).toBeVisible();
722
+ expect( modalContent.getByText( '17px' ) ).toBeVisible();
723
+ expect( modalContent.getByLabelText( 'Large' ) ).toBeVisible();
724
+ expect( modalContent.getByText( '30px' ) ).toBeVisible();
725
+ expect( modalContent.getByLabelText( 'Extra Large' ) ).toBeVisible();
726
+ expect( modalContent.getByText( '40px' ) ).toBeVisible();
727
+ expect(
728
+ modalContent.getByLabelText( 'Extra Extra Large' )
729
+ ).toBeVisible();
730
+ expect( modalContent.getByText( '52px' ) ).toBeVisible();
731
+ } );
732
+
733
+ it( 'should set a font size value', async () => {
734
+ // Arrange
735
+ const screen = await initializeEditor( { withGlobalStyles: true } );
736
+ await addBlock( screen, 'Paragraph' );
737
+
738
+ // Act
739
+ const paragraphBlock = getBlock( screen, 'Paragraph' );
740
+ fireEvent.press( paragraphBlock );
741
+ const paragraphTextInput =
742
+ within( paragraphBlock ).getByPlaceholderText( 'Start writing…' );
743
+ typeInRichText(
744
+ paragraphTextInput,
745
+ 'A quick brown fox jumps over the lazy dog.'
746
+ );
747
+ // Open Block Settings.
748
+ fireEvent.press( screen.getByLabelText( 'Open Settings' ) );
749
+
750
+ // Wait for Block Settings to be visible.
751
+ const blockSettingsModal = screen.getByTestId( 'block-settings-modal' );
752
+ await waitForModalVisible( blockSettingsModal );
753
+
754
+ // Open Font size settings
755
+ fireEvent.press( screen.getByLabelText( 'Font Size, Custom' ) );
756
+
757
+ // Tap one font size
758
+ fireEvent.press( screen.getByLabelText( 'Large' ) );
759
+
760
+ // Dismiss the Block Settings modal.
761
+ await dismissModal( blockSettingsModal );
762
+
763
+ // Assert
764
+ expect( getEditorHtml() ).toMatchSnapshot();
765
+ } );
766
+
767
+ it( 'should set a line height value', async () => {
768
+ // Arrange
769
+ const screen = await initializeEditor( { withGlobalStyles: true } );
770
+ await addBlock( screen, 'Paragraph' );
771
+
772
+ // Act
773
+ const paragraphBlock = getBlock( screen, 'Paragraph' );
774
+ fireEvent.press( paragraphBlock );
775
+ const paragraphTextInput =
776
+ within( paragraphBlock ).getByPlaceholderText( 'Start writing…' );
777
+ typeInRichText(
778
+ paragraphTextInput,
779
+ 'A quick brown fox jumps over the lazy dog.'
780
+ );
781
+ // Open Block Settings.
782
+ fireEvent.press( screen.getByLabelText( 'Open Settings' ) );
783
+
784
+ // Wait for Block Settings to be visible.
785
+ const blockSettingsModal = screen.getByTestId( 'block-settings-modal' );
786
+ await waitForModalVisible( blockSettingsModal );
787
+
788
+ const lineHeightControl = screen.getByLabelText( /Line Height/ );
789
+ fireEvent.press(
790
+ within( lineHeightControl ).getByText( '1.5', { hidden: true } )
791
+ );
792
+ const lineHeightTextInput = within(
793
+ lineHeightControl
794
+ ).getByDisplayValue( '1.5', { hidden: true } );
795
+ fireEvent.changeText( lineHeightTextInput, '1.8' );
796
+
797
+ // Dismiss the Block Settings modal.
798
+ await dismissModal( blockSettingsModal );
799
+
800
+ // Assert
801
+ expect( getEditorHtml() ).toMatchSnapshot();
802
+ } );
803
+
690
804
  it( 'should focus on the previous Paragraph block when backspacing in an empty Paragraph block', async () => {
691
805
  // Arrange
692
806
  const screen = await initializeEditor();
@@ -3,12 +3,19 @@
3
3
  */
4
4
  import { cloneBlock } from '@wordpress/blocks';
5
5
  import { useSelect, useDispatch } from '@wordpress/data';
6
- import { useEffect } from '@wordpress/element';
6
+ import { useState, useEffect } from '@wordpress/element';
7
7
  import {
8
+ Warning,
8
9
  store as blockEditorStore,
9
10
  useBlockProps,
10
11
  } from '@wordpress/block-editor';
11
12
  import { store as coreStore } from '@wordpress/core-data';
13
+ import { __, sprintf } from '@wordpress/i18n';
14
+
15
+ /**
16
+ * Internal dependencies
17
+ */
18
+ import { useParsePatternDependencies } from './recursion-detector';
12
19
 
13
20
  const PatternEdit = ( { attributes, clientId } ) => {
14
21
  const selectedPattern = useSelect(
@@ -32,6 +39,9 @@ const PatternEdit = ( { attributes, clientId } ) => {
32
39
  const { getBlockRootClientId, getBlockEditingMode } =
33
40
  useSelect( blockEditorStore );
34
41
 
42
+ const [ hasRecursionError, setHasRecursionError ] = useState( false );
43
+ const parsePatternDependencies = useParsePatternDependencies();
44
+
35
45
  // Duplicated in packages/edit-site/src/components/start-template-options/index.js.
36
46
  function injectThemeAttributeInBlockTemplateContent( block ) {
37
47
  if (
@@ -64,7 +74,14 @@ const PatternEdit = ( { attributes, clientId } ) => {
64
74
  // This change won't be saved.
65
75
  // It will continue to pull from the pattern file unless changes are made to its respective template part.
66
76
  useEffect( () => {
67
- if ( selectedPattern?.blocks ) {
77
+ if ( ! hasRecursionError && selectedPattern?.blocks ) {
78
+ try {
79
+ parsePatternDependencies( selectedPattern );
80
+ } catch ( error ) {
81
+ setHasRecursionError( true );
82
+ return;
83
+ }
84
+
68
85
  // We batch updates to block list settings to avoid triggering cascading renders
69
86
  // for each container block included in a tree and optimize initial render.
70
87
  // Since the above uses microtasks, we need to use a microtask here as well,
@@ -93,7 +110,8 @@ const PatternEdit = ( { attributes, clientId } ) => {
93
110
  }
94
111
  }, [
95
112
  clientId,
96
- selectedPattern?.blocks,
113
+ hasRecursionError,
114
+ selectedPattern,
97
115
  __unstableMarkNextChangeAsNotPersistent,
98
116
  replaceBlocks,
99
117
  getBlockEditingMode,
@@ -103,6 +121,20 @@ const PatternEdit = ( { attributes, clientId } ) => {
103
121
 
104
122
  const props = useBlockProps();
105
123
 
124
+ if ( hasRecursionError ) {
125
+ return (
126
+ <div { ...props }>
127
+ <Warning>
128
+ { sprintf(
129
+ // translators: A warning in which %s is the name of a pattern.
130
+ __( 'Pattern "%s" cannot be rendered inside itself.' ),
131
+ selectedPattern?.name
132
+ ) }
133
+ </Warning>
134
+ </div>
135
+ );
136
+ }
137
+
106
138
  return <div { ...props } />;
107
139
  };
108
140
 
@@ -27,6 +27,8 @@ function register_block_core_pattern() {
27
27
  * @return string Returns the output of the pattern.
28
28
  */
29
29
  function render_block_core_pattern( $attributes ) {
30
+ static $seen_refs = array();
31
+
30
32
  if ( empty( $attributes['slug'] ) ) {
31
33
  return '';
32
34
  }
@@ -38,6 +40,17 @@ function render_block_core_pattern( $attributes ) {
38
40
  return '';
39
41
  }
40
42
 
43
+ if ( isset( $seen_refs[ $attributes['slug'] ] ) ) {
44
+ // WP_DEBUG_DISPLAY must only be honored when WP_DEBUG. This precedent
45
+ // is set in `wp_debug_mode()`.
46
+ $is_debug = WP_DEBUG && WP_DEBUG_DISPLAY;
47
+
48
+ return $is_debug ?
49
+ // translators: Visible only in the front end, this warning takes the place of a faulty block. %s represents a pattern's slug.
50
+ sprintf( __( '[block rendering halted for pattern "%s"]' ), $slug ) :
51
+ '';
52
+ }
53
+
41
54
  $pattern = $registry->get_registered( $slug );
42
55
  $content = $pattern['content'];
43
56
 
@@ -48,11 +61,14 @@ function render_block_core_pattern( $attributes ) {
48
61
  $content = gutenberg_serialize_blocks( $blocks );
49
62
  }
50
63
 
64
+ $seen_refs[ $attributes['slug'] ] = true;
65
+
51
66
  $content = do_blocks( $content );
52
67
 
53
68
  global $wp_embed;
54
69
  $content = $wp_embed->autoembed( $content );
55
70
 
71
+ unset( $seen_refs[ $attributes['slug'] ] );
56
72
  return $content;
57
73
  }
58
74
 
@@ -0,0 +1,145 @@
1
+ /**
2
+ * THIS MODULE IS INTENTIONALLY KEPT WITHIN THE PATTERN BLOCK'S SOURCE.
3
+ *
4
+ * This is because this approach for preventing infinite loops due to
5
+ * recursively rendering blocks is specific to the way that the `core/pattern`
6
+ * block behaves in the editor. Any other block types that deal with recursion
7
+ * SHOULD USE THE STANDARD METHOD for avoiding loops:
8
+ *
9
+ * @see https://github.com/WordPress/gutenberg/pull/31455
10
+ * @see packages/block-editor/src/components/recursion-provider/README.md
11
+ */
12
+
13
+ /**
14
+ * WordPress dependencies
15
+ */
16
+ import { useRegistry } from '@wordpress/data';
17
+
18
+ /**
19
+ * Naming is hard.
20
+ *
21
+ * @see useParsePatternDependencies
22
+ *
23
+ * @type {WeakMap<Object, Function>}
24
+ */
25
+ const cachedParsers = new WeakMap();
26
+
27
+ /**
28
+ * Hook used by PatternEdit to parse block patterns. It returns a function that
29
+ * takes a pattern and returns nothing but throws an error if the pattern is
30
+ * recursive.
31
+ *
32
+ * @example
33
+ * ```js
34
+ * const parsePatternDependencies = useParsePatternDependencies();
35
+ * parsePatternDependencies( selectedPattern );
36
+ * ```
37
+ *
38
+ * @see parsePatternDependencies
39
+ *
40
+ * @return {Function} A function to parse block patterns.
41
+ */
42
+ export function useParsePatternDependencies() {
43
+ const registry = useRegistry();
44
+
45
+ // Instead of caching maps, go straight to the point and cache bound
46
+ // functions. Each of those functions is bound to a different Map that will
47
+ // keep track of patterns in the context of the given registry.
48
+ if ( ! cachedParsers.has( registry ) ) {
49
+ const deps = new Map();
50
+ cachedParsers.set(
51
+ registry,
52
+ parsePatternDependencies.bind( null, deps )
53
+ );
54
+ }
55
+ return cachedParsers.get( registry );
56
+ }
57
+
58
+ /**
59
+ * Parse a given pattern and traverse its contents to detect any subsequent
60
+ * patterns on which it may depend. Such occurrences will be added to an
61
+ * internal dependency graph. If a circular dependency is detected, an
62
+ * error will be thrown.
63
+ *
64
+ * EXPORTED FOR TESTING PURPOSES ONLY.
65
+ *
66
+ * @param {Map<string, Set<string>>} deps Map of pattern dependencies.
67
+ * @param {Object} pattern Pattern.
68
+ * @param {string} pattern.name Pattern name.
69
+ * @param {Array} pattern.blocks Pattern's block list.
70
+ *
71
+ * @throws {Error} If a circular dependency is detected.
72
+ */
73
+ export function parsePatternDependencies( deps, { name, blocks } ) {
74
+ const queue = [ ...blocks ];
75
+ while ( queue.length ) {
76
+ const block = queue.shift();
77
+ for ( const innerBlock of block.innerBlocks ?? [] ) {
78
+ queue.unshift( innerBlock );
79
+ }
80
+ if ( block.name === 'core/pattern' ) {
81
+ registerDependency( deps, name, block.attributes.slug );
82
+ }
83
+ }
84
+ }
85
+
86
+ /**
87
+ * Declare that pattern `a` depends on pattern `b`. If a circular
88
+ * dependency is detected, an error will be thrown.
89
+ *
90
+ * EXPORTED FOR TESTING PURPOSES ONLY.
91
+ *
92
+ * @param {Map<string, Set<string>>} deps Map of pattern dependencies.
93
+ * @param {string} a Slug for pattern A.
94
+ * @param {string} b Slug for pattern B.
95
+ *
96
+ * @throws {Error} If a circular dependency is detected.
97
+ */
98
+ export function registerDependency( deps, a, b ) {
99
+ if ( ! deps.has( a ) ) {
100
+ deps.set( a, new Set() );
101
+ }
102
+ deps.get( a ).add( b );
103
+ if ( hasCycle( deps, a ) ) {
104
+ throw new TypeError(
105
+ `Pattern ${ a } has a circular dependency and cannot be rendered.`
106
+ );
107
+ }
108
+ }
109
+
110
+ /**
111
+ * Determine if a given pattern has circular dependencies on other patterns.
112
+ * This will be determined by running a depth-first search on the current state
113
+ * of the graph represented by `patternDependencies`.
114
+ *
115
+ * @param {Map<string, Set<string>>} deps Map of pattern dependencies.
116
+ * @param {string} slug Pattern slug.
117
+ * @param {Set<string>} [visitedNodes] Set to track visited nodes in the graph.
118
+ * @param {Set<string>} [currentPath] Set to track and backtrack graph paths.
119
+ * @return {boolean} Whether any cycle was found.
120
+ */
121
+ function hasCycle(
122
+ deps,
123
+ slug,
124
+ visitedNodes = new Set(),
125
+ currentPath = new Set()
126
+ ) {
127
+ visitedNodes.add( slug );
128
+ currentPath.add( slug );
129
+
130
+ const dependencies = deps.get( slug ) ?? new Set();
131
+
132
+ for ( const dependency of dependencies ) {
133
+ if ( ! visitedNodes.has( dependency ) ) {
134
+ if ( hasCycle( deps, dependency, visitedNodes, currentPath ) ) {
135
+ return true;
136
+ }
137
+ } else if ( currentPath.has( dependency ) ) {
138
+ return true;
139
+ }
140
+ }
141
+
142
+ // Remove the current node from the current path when backtracking
143
+ currentPath.delete( slug );
144
+ return false;
145
+ }
@@ -0,0 +1,74 @@
1
+ /**
2
+ * Internal dependencies
3
+ */
4
+ import {
5
+ parsePatternDependencies,
6
+ registerDependency,
7
+ } from '../recursion-detector';
8
+
9
+ describe( 'core/pattern', () => {
10
+ const deps = new Map();
11
+
12
+ beforeEach( () => {
13
+ deps.clear();
14
+ } );
15
+
16
+ describe( 'parsePatternDependencies', () => {
17
+ it( "is silent for patterns that don't require other patterns", () => {
18
+ const pattern = {
19
+ name: 'test/benign-pattern',
20
+ blocks: [ { name: 'core/paragraph' } ],
21
+ };
22
+ expect( () => {
23
+ parsePatternDependencies( deps, pattern );
24
+ } ).not.toThrow();
25
+ } );
26
+ it( 'catches self-referencing patterns', () => {
27
+ const pattern = {
28
+ name: 'test/evil-pattern',
29
+ blocks: [ { name: 'core/pattern', slug: 'test/evil-pattern' } ],
30
+ };
31
+ expect( () => {
32
+ parsePatternDependencies( deps, pattern );
33
+ } ).toThrow();
34
+ } );
35
+ } );
36
+
37
+ describe( 'registerDependency', () => {
38
+ it( 'is silent for patterns with no circular dependencies', () => {
39
+ expect( () => {
40
+ registerDependency( deps, 'a', 'b' );
41
+ } ).not.toThrow();
42
+ } );
43
+ it( 'catches self-referencing patterns', () => {
44
+ expect( () => {
45
+ registerDependency( deps, 'a', 'a' );
46
+ } ).toThrow();
47
+ } );
48
+ it( 'catches mutually-referencing patterns', () => {
49
+ registerDependency( deps, 'a', 'b' );
50
+ expect( () => {
51
+ registerDependency( deps, 'b', 'a' );
52
+ } ).toThrow();
53
+ } );
54
+ it( 'catches longer cycles', () => {
55
+ registerDependency( deps, 'a', 'b' );
56
+ registerDependency( deps, 'b', 'c' );
57
+ registerDependency( deps, 'b', 'd' );
58
+ expect( () => {
59
+ registerDependency( deps, 'd', 'a' );
60
+ } ).toThrow();
61
+ } );
62
+ it( 'catches any pattern depending on a tainted one', () => {
63
+ registerDependency( deps, 'a', 'b' );
64
+ registerDependency( deps, 'b', 'c' );
65
+ registerDependency( deps, 'b', 'd' );
66
+ expect( () => {
67
+ registerDependency( deps, 'd', 'a' );
68
+ } ).toThrow();
69
+ expect( () => {
70
+ registerDependency( deps, 'e', 'd' );
71
+ } ).toThrow();
72
+ } );
73
+ } );
74
+ } );
@@ -51,6 +51,10 @@
51
51
  },
52
52
  "customGradient": {
53
53
  "type": "string"
54
+ },
55
+ "useFirstImageFromPost": {
56
+ "type": "boolean",
57
+ "default": false
54
58
  }
55
59
  },
56
60
  "usesContext": [ "postId", "postType", "queryId" ],
@@ -25,6 +25,7 @@ import {
25
25
  store as blockEditorStore,
26
26
  __experimentalUseBorderProps as useBorderProps,
27
27
  } from '@wordpress/block-editor';
28
+ import { useMemo } from '@wordpress/element';
28
29
  import { __, sprintf } from '@wordpress/i18n';
29
30
  import { upload } from '@wordpress/icons';
30
31
  import { store as noticesStore } from '@wordpress/notices';
@@ -64,14 +65,44 @@ export default function PostFeaturedImageEdit( {
64
65
  sizeSlug,
65
66
  rel,
66
67
  linkTarget,
68
+ useFirstImageFromPost,
67
69
  } = attributes;
68
- const [ featuredImage, setFeaturedImage ] = useEntityProp(
70
+
71
+ const [ storedFeaturedImage, setFeaturedImage ] = useEntityProp(
69
72
  'postType',
70
73
  postTypeSlug,
71
74
  'featured_media',
72
75
  postId
73
76
  );
74
77
 
78
+ // Fallback to post content if no featured image is set.
79
+ // This is needed for the "Use first image from post" option.
80
+ const [ postContent ] = useEntityProp(
81
+ 'postType',
82
+ postTypeSlug,
83
+ 'content',
84
+ postId
85
+ );
86
+
87
+ const featuredImage = useMemo( () => {
88
+ if ( storedFeaturedImage ) {
89
+ return storedFeaturedImage;
90
+ }
91
+
92
+ if ( ! useFirstImageFromPost ) {
93
+ return;
94
+ }
95
+
96
+ const imageOpener =
97
+ /<!--\s+wp:(?:core\/)?image\s+(?<attrs>{(?:(?:[^}]+|}+(?=})|(?!}\s+\/?-->).)*)?}\s+)?-->/.exec(
98
+ postContent
99
+ );
100
+ const imageId =
101
+ imageOpener?.groups?.attrs &&
102
+ JSON.parse( imageOpener.groups.attrs )?.id;
103
+ return imageId;
104
+ }, [ storedFeaturedImage, useFirstImageFromPost, postContent ] );
105
+
75
106
  const { media, postType, postPermalink } = useSelect(
76
107
  ( select ) => {
77
108
  const { getMedia, getPostType, getEditedEntityRecord } =
@@ -54,9 +54,40 @@ function render_block_core_post_featured_image( $attributes, $content, $block )
54
54
  }
55
55
 
56
56
  $featured_image = get_the_post_thumbnail( $post_ID, $size_slug, $attr );
57
+
58
+ // Get the first image from the post.
59
+ if ( $attributes['useFirstImageFromPost'] && ! $featured_image ) {
60
+ $content_post = get_post( $post_ID );
61
+ $content = $content_post->post_content;
62
+ $processor = new WP_HTML_Tag_Processor( $content );
63
+
64
+ /*
65
+ * Transfer the image tag from the post into a new text snippet.
66
+ * Because the HTML API doesn't currently expose a way to extract
67
+ * HTML substrings this is necessary as a workaround. Of note, this
68
+ * is different than directly extracting the IMG tag:
69
+ * - If there are duplicate attributes in the source there will only be one in the output.
70
+ * - If there are single-quoted or unquoted attributes they will be double-quoted in the output.
71
+ * - If there are named character references in the attribute values they may be replaced with their direct code points. E.g. `&hellip;` becomes `…`.
72
+ * In the future there will likely be a mechanism to copy snippets of HTML from
73
+ * one document into another, via the HTML Processor's `get_outer_html()` or
74
+ * equivalent. When that happens it would be appropriate to replace this custom
75
+ * code with that canonical code.
76
+ */
77
+ if ( $processor->next_tag( 'img' ) ) {
78
+ $tag_html = new WP_HTML_Tag_Processor( '<img>' );
79
+ $tag_html->next_tag();
80
+ foreach ( $processor->get_attribute_names_with_prefix( '' ) as $name ) {
81
+ $tag_html->set_attribute( $name, $processor->get_attribute( $name ) );
82
+ }
83
+ $featured_image = $tag_html->get_updated_html();
84
+ }
85
+ }
86
+
57
87
  if ( ! $featured_image ) {
58
88
  return '';
59
89
  }
90
+
60
91
  if ( $is_link ) {
61
92
  $link_target = $attributes['linkTarget'];
62
93
  $rel = ! empty( $attributes['rel'] ) ? 'rel="' . esc_attr( $attributes['rel'] ) . '"' : '';
@@ -37,6 +37,7 @@ import {
37
37
  isControlAllowed,
38
38
  useTaxonomies,
39
39
  } from '../../utils';
40
+ import { TOOLSPANEL_DROPDOWNMENU_PROPS } from '../../../utils/constants';
40
41
 
41
42
  const { BlockInfo } = unlock( blockEditorPrivateApis );
42
43
 
@@ -226,6 +227,7 @@ export default function QueryInspectorControls( props ) {
226
227
  } );
227
228
  setQuerySearch( '' );
228
229
  } }
230
+ dropdownMenuProps={ TOOLSPANEL_DROPDOWNMENU_PROPS }
229
231
  >
230
232
  { showTaxControl && (
231
233
  <ToolsPanelItem
@@ -5,7 +5,7 @@
5
5
  "title": "Page Numbers",
6
6
  "category": "theme",
7
7
  "parent": [ "core/query-pagination" ],
8
- "description": "Displays a list of page numbers for pagination",
8
+ "description": "Displays a list of page numbers for pagination.",
9
9
  "textdomain": "default",
10
10
  "attributes": {
11
11
  "midSize": {
@@ -43,10 +43,6 @@
43
43
  "type": "object",
44
44
  "default": {}
45
45
  },
46
- "buttonBehavior": {
47
- "type": "string",
48
- "default": "expand-searchfield"
49
- },
50
46
  "isSearchFieldHidden": {
51
47
  "type": "boolean",
52
48
  "default": false
@@ -59,8 +59,6 @@ import {
59
59
  // button is placed inside wrapper.
60
60
  const DEFAULT_INNER_PADDING = '4px';
61
61
 
62
- const BUTTON_BEHAVIOR_EXPAND = 'expand-searchfield';
63
-
64
62
  export default function SearchEdit( {
65
63
  className,
66
64
  attributes,
@@ -79,7 +77,6 @@ export default function SearchEdit( {
79
77
  buttonText,
80
78
  buttonPosition,
81
79
  buttonUseIcon,
82
- buttonBehavior,
83
80
  isSearchFieldHidden,
84
81
  style,
85
82
  } = attributes;
@@ -187,9 +184,6 @@ export default function SearchEdit( {
187
184
  buttonUseIcon && ! hasNoButton
188
185
  ? 'wp-block-search__icon-button'
189
186
  : undefined,
190
- hasOnlyButton && BUTTON_BEHAVIOR_EXPAND === buttonBehavior
191
- ? 'wp-block-search__button-behavior-expand'
192
- : undefined,
193
187
  hasOnlyButton && isSearchFieldHidden
194
188
  ? 'wp-block-search__searchfield-hidden'
195
189
  : undefined
@@ -325,7 +319,7 @@ export default function SearchEdit( {
325
319
  : borderProps.style ),
326
320
  };
327
321
  const handleButtonClick = () => {
328
- if ( hasOnlyButton && BUTTON_BEHAVIOR_EXPAND === buttonBehavior ) {
322
+ if ( hasOnlyButton ) {
329
323
  setAttributes( {
330
324
  isSearchFieldHidden: ! isSearchFieldHidden,
331
325
  } );
@@ -453,7 +447,7 @@ export default function SearchEdit( {
453
447
  return (
454
448
  <Button
455
449
  key={ widthValue }
456
- isSmall
450
+ size="small"
457
451
  variant={
458
452
  widthValue === width &&
459
453
  widthUnit === '%'