@wordpress/block-library 9.34.1-next.2f1c7c01b.0 → 9.35.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 (149) hide show
  1. package/CHANGELOG.md +2 -0
  2. package/build/block/edit.js +2 -2
  3. package/build/block/edit.js.map +2 -2
  4. package/build/block-keyboard-shortcuts/index.js +17 -7
  5. package/build/block-keyboard-shortcuts/index.js.map +2 -2
  6. package/build/cover/deprecated.js +15 -3
  7. package/build/cover/deprecated.js.map +2 -2
  8. package/build/cover/edit/inspector-controls.js +1 -1
  9. package/build/cover/edit/inspector-controls.js.map +2 -2
  10. package/build/cover/transforms.js +10 -2
  11. package/build/cover/transforms.js.map +2 -2
  12. package/build/embed/icons.js +2 -2
  13. package/build/embed/icons.js.map +2 -2
  14. package/build/embed/variations.js +3 -3
  15. package/build/embed/variations.js.map +2 -2
  16. package/build/heading/index.js +3 -1
  17. package/build/heading/index.js.map +3 -3
  18. package/build/heading/transforms.js +10 -3
  19. package/build/heading/transforms.js.map +2 -2
  20. package/build/heading/variations.js +55 -0
  21. package/build/heading/variations.js.map +7 -0
  22. package/build/html/edit.js +54 -44
  23. package/build/html/edit.js.map +3 -3
  24. package/build/html/modal.js +328 -0
  25. package/build/html/modal.js.map +7 -0
  26. package/build/html/utils.js +72 -0
  27. package/build/html/utils.js.map +7 -0
  28. package/build/navigation-link/edit.js +25 -10
  29. package/build/navigation-link/edit.js.map +2 -2
  30. package/build/navigation-link/link-ui/index.js +8 -3
  31. package/build/navigation-link/link-ui/index.js.map +2 -2
  32. package/build/navigation-link/shared/controls.js +42 -7
  33. package/build/navigation-link/shared/controls.js.map +2 -2
  34. package/build/navigation-link/shared/use-entity-binding.js +31 -2
  35. package/build/navigation-link/shared/use-entity-binding.js.map +3 -3
  36. package/build/paragraph/block.json +1 -3
  37. package/build/paragraph/deprecated.js +65 -12
  38. package/build/paragraph/deprecated.js.map +2 -2
  39. package/build/paragraph/edit.js +14 -25
  40. package/build/paragraph/edit.js.map +2 -2
  41. package/build/paragraph/index.js +3 -1
  42. package/build/paragraph/index.js.map +3 -3
  43. package/build/paragraph/save.js +3 -3
  44. package/build/paragraph/save.js.map +2 -2
  45. package/build/paragraph/transforms.js +7 -1
  46. package/build/paragraph/transforms.js.map +2 -2
  47. package/build/paragraph/variations.js +57 -0
  48. package/build/paragraph/variations.js.map +7 -0
  49. package/build-module/block/edit.js +2 -2
  50. package/build-module/block/edit.js.map +2 -2
  51. package/build-module/block-keyboard-shortcuts/index.js +17 -7
  52. package/build-module/block-keyboard-shortcuts/index.js.map +2 -2
  53. package/build-module/cover/deprecated.js +15 -3
  54. package/build-module/cover/deprecated.js.map +2 -2
  55. package/build-module/cover/edit/inspector-controls.js +1 -1
  56. package/build-module/cover/edit/inspector-controls.js.map +2 -2
  57. package/build-module/cover/transforms.js +10 -2
  58. package/build-module/cover/transforms.js.map +2 -2
  59. package/build-module/embed/icons.js +2 -2
  60. package/build-module/embed/icons.js.map +2 -2
  61. package/build-module/embed/variations.js +3 -3
  62. package/build-module/embed/variations.js.map +2 -2
  63. package/build-module/heading/index.js +3 -1
  64. package/build-module/heading/index.js.map +2 -2
  65. package/build-module/heading/transforms.js +10 -3
  66. package/build-module/heading/transforms.js.map +2 -2
  67. package/build-module/heading/variations.js +34 -0
  68. package/build-module/heading/variations.js.map +7 -0
  69. package/build-module/html/edit.js +62 -51
  70. package/build-module/html/edit.js.map +2 -2
  71. package/build-module/html/modal.js +304 -0
  72. package/build-module/html/modal.js.map +7 -0
  73. package/build-module/html/utils.js +46 -0
  74. package/build-module/html/utils.js.map +7 -0
  75. package/build-module/navigation-link/edit.js +25 -10
  76. package/build-module/navigation-link/edit.js.map +2 -2
  77. package/build-module/navigation-link/link-ui/index.js +8 -3
  78. package/build-module/navigation-link/link-ui/index.js.map +2 -2
  79. package/build-module/navigation-link/shared/controls.js +42 -7
  80. package/build-module/navigation-link/shared/controls.js.map +2 -2
  81. package/build-module/navigation-link/shared/use-entity-binding.js +35 -3
  82. package/build-module/navigation-link/shared/use-entity-binding.js.map +2 -2
  83. package/build-module/paragraph/block.json +1 -3
  84. package/build-module/paragraph/deprecated.js +65 -12
  85. package/build-module/paragraph/deprecated.js.map +2 -2
  86. package/build-module/paragraph/edit.js +14 -26
  87. package/build-module/paragraph/edit.js.map +2 -2
  88. package/build-module/paragraph/index.js +3 -1
  89. package/build-module/paragraph/index.js.map +2 -2
  90. package/build-module/paragraph/save.js +3 -3
  91. package/build-module/paragraph/save.js.map +2 -2
  92. package/build-module/paragraph/transforms.js +7 -1
  93. package/build-module/paragraph/transforms.js.map +2 -2
  94. package/build-module/paragraph/variations.js +36 -0
  95. package/build-module/paragraph/variations.js.map +7 -0
  96. package/build-style/accordion-heading/style-rtl.css +19 -3
  97. package/build-style/accordion-heading/style.css +19 -3
  98. package/build-style/accordion-panel/style-rtl.css +4 -1
  99. package/build-style/accordion-panel/style.css +4 -1
  100. package/build-style/common-rtl.css +3 -3
  101. package/build-style/common.css +3 -3
  102. package/build-style/editor-rtl.css +62 -21
  103. package/build-style/editor.css +62 -21
  104. package/build-style/embed/style-rtl.css +5 -0
  105. package/build-style/embed/style.css +5 -0
  106. package/build-style/html/editor-rtl.css +55 -21
  107. package/build-style/html/editor.css +55 -21
  108. package/build-style/navigation-link/editor-rtl.css +7 -0
  109. package/build-style/navigation-link/editor.css +7 -0
  110. package/build-style/style-rtl.css +31 -7
  111. package/build-style/style.css +31 -7
  112. package/package.json +37 -37
  113. package/src/accordion-heading/style.scss +40 -7
  114. package/src/accordion-panel/style.scss +6 -1
  115. package/src/block/edit.js +2 -2
  116. package/src/block-keyboard-shortcuts/index.js +23 -9
  117. package/src/common.scss +6 -5
  118. package/src/cover/deprecated.js +15 -3
  119. package/src/cover/edit/inspector-controls.js +1 -1
  120. package/src/cover/transforms.js +10 -2
  121. package/src/embed/icons.js +2 -4
  122. package/src/embed/style.scss +6 -0
  123. package/src/embed/variations.js +3 -3
  124. package/src/heading/index.js +2 -0
  125. package/src/heading/transforms.js +10 -3
  126. package/src/heading/variations.js +37 -0
  127. package/src/html/edit.js +62 -56
  128. package/src/html/editor.scss +69 -10
  129. package/src/html/modal.js +290 -0
  130. package/src/html/test/utils.js +234 -0
  131. package/src/html/utils.js +75 -0
  132. package/src/navigation-link/edit.js +44 -13
  133. package/src/navigation-link/editor.scss +7 -0
  134. package/src/navigation-link/index.php +65 -2
  135. package/src/navigation-link/link-ui/index.js +9 -8
  136. package/src/navigation-link/shared/controls.js +70 -12
  137. package/src/navigation-link/shared/test/controls.js +5 -0
  138. package/src/navigation-link/shared/test/use-entity-binding.js +14 -1
  139. package/src/navigation-link/shared/use-entity-binding.js +57 -9
  140. package/src/paragraph/block.json +1 -3
  141. package/src/paragraph/deprecated.js +87 -20
  142. package/src/paragraph/edit.js +7 -18
  143. package/src/paragraph/edit.native.js +18 -6
  144. package/src/paragraph/index.js +2 -0
  145. package/src/paragraph/save.js +4 -3
  146. package/src/paragraph/test/edit.native.js +5 -5
  147. package/src/paragraph/transforms.js +7 -1
  148. package/src/paragraph/variations.js +39 -0
  149. package/tsconfig.tsbuildinfo +1 -1
@@ -1,4 +1,6 @@
1
1
  @use "@wordpress/base-styles/mixins" as *;
2
+ @use "@wordpress/base-styles/variables" as *;
3
+ @use "@wordpress/base-styles/colors" as *;
2
4
 
3
5
  .block-library-html__edit {
4
6
  .block-library-html__preview-overlay {
@@ -8,16 +10,73 @@
8
10
  top: 0;
9
11
  left: 0;
10
12
  }
13
+ }
14
+
15
+ // Modal styles
16
+ .block-library-html__modal {
17
+ // Make modal content scrollable
18
+ .components-modal__content {
19
+ display: flex;
20
+ flex-direction: column;
21
+ padding: 0;
22
+ min-height: 70vh;
23
+ }
24
+
25
+ .components-modal__children-container {
26
+ height: 100%;
27
+ padding: $grid-unit-20;
28
+ }
29
+ }
30
+
31
+ .block-library-html__modal-tabs {
32
+ height: 100%;
33
+ }
34
+
35
+ .block-library-html__modal-tab {
36
+ height: 100%;
37
+ display: flex;
38
+ flex-direction: column;
39
+ margin: 0;
40
+ box-sizing: border-box;
41
+ border: 1px solid $gray-200;
42
+ border-radius: 2px;
43
+ padding: $grid-unit-20;
44
+ font-family: $editor-html-font;
45
+ }
46
+
47
+ .block-library-html__modal-editor {
48
+ width: 100%;
49
+ height: 100%;
50
+ flex: 1;
51
+ // Reset textarea styles to inherit from pre
52
+ border: none;
53
+ background: transparent;
54
+ padding: 0;
55
+ font-family: inherit;
56
+ font-size: inherit;
57
+ line-height: inherit;
58
+ color: inherit;
59
+ resize: none;
60
+ // HTML input is always LTR regardless of language.
61
+ /*rtl:ignore*/
62
+ direction: ltr;
63
+ overflow-x: auto;
64
+ box-sizing: border-box;
11
65
 
12
- // The editing view for the HTML block is equivalent to block UI.
13
- // Therefore we increase specificity to avoid theme styles bleeding in.
14
- .block-editor-plain-text {
15
- display: block;
16
- box-sizing: border-box;
17
- max-height: 250px;
18
- @include editor-input-reset();
19
- // HTML input is always LTR regardless of language.
20
- /*rtl:ignore*/
21
- direction: ltr;
66
+ &:focus {
67
+ outline: none;
68
+ box-shadow: none;
22
69
  }
23
70
  }
71
+
72
+ .block-library-html__preview {
73
+ display: flex;
74
+ align-items: center;
75
+ justify-content: center;
76
+ padding: $grid-unit-60;
77
+ }
78
+
79
+ .block-library-html__modal-actions {
80
+ margin-top: $grid-unit-20;
81
+ padding: 0 $grid-unit-40 $grid-unit-40;
82
+ }
@@ -0,0 +1,290 @@
1
+ /**
2
+ * WordPress dependencies
3
+ */
4
+ import { __ } from '@wordpress/i18n';
5
+ import { useState, useMemo } from '@wordpress/element';
6
+ import { useSelect } from '@wordpress/data';
7
+ import {
8
+ Modal,
9
+ Button,
10
+ Flex,
11
+ privateApis as componentsPrivateApis,
12
+ __experimentalHStack as HStack,
13
+ __experimentalVStack as VStack,
14
+ } from '@wordpress/components';
15
+ import { PlainText, store as blockEditorStore } from '@wordpress/block-editor';
16
+ import { fullscreen, square } from '@wordpress/icons';
17
+
18
+ /**
19
+ * Internal dependencies
20
+ */
21
+ import { unlock } from '../lock-unlock';
22
+ import Preview from './preview';
23
+ import { parseContent, serializeContent } from './utils';
24
+
25
+ const { Tabs } = unlock( componentsPrivateApis );
26
+
27
+ export default function HTMLEditModal( {
28
+ isOpen,
29
+ onRequestClose,
30
+ content,
31
+ setAttributes,
32
+ } ) {
33
+ // Parse content into separate sections and use as initial state
34
+ const { html, css, js } = parseContent( content );
35
+ const [ editedHtml, setEditedHtml ] = useState( html );
36
+ const [ editedCss, setEditedCss ] = useState( css );
37
+ const [ editedJs, setEditedJs ] = useState( js );
38
+ const [ isDirty, setIsDirty ] = useState( false );
39
+ const [ showUnsavedWarning, setShowUnsavedWarning ] = useState( false );
40
+ const [ isFullscreen, setIsFullscreen ] = useState( false );
41
+
42
+ // Check if user has permission to save scripts and get editor styles
43
+ const { canUserUseUnfilteredHTML, editorStyles } = useSelect(
44
+ ( select ) => {
45
+ const settings = select( blockEditorStore ).getSettings();
46
+ return {
47
+ canUserUseUnfilteredHTML:
48
+ settings.__experimentalCanUserUseUnfilteredHTML,
49
+ editorStyles: settings.styles,
50
+ };
51
+ },
52
+ []
53
+ );
54
+
55
+ // Show JS tab if user has permission OR if block contains JavaScript
56
+ const shouldShowJsTab = canUserUseUnfilteredHTML || js.trim() !== '';
57
+
58
+ // Combine all editor styles to inject into modal
59
+ const styleContent = useMemo( () => {
60
+ if ( ! editorStyles ) {
61
+ return '';
62
+ }
63
+ return editorStyles
64
+ .filter( ( style ) => style.css )
65
+ .map( ( style ) => style.css )
66
+ .join( '\n' );
67
+ }, [ editorStyles ] );
68
+
69
+ if ( ! isOpen ) {
70
+ return null;
71
+ }
72
+
73
+ const handleHtmlChange = ( value ) => {
74
+ setEditedHtml( value );
75
+ setIsDirty( true );
76
+ };
77
+ const handleCssChange = ( value ) => {
78
+ setEditedCss( value );
79
+ setIsDirty( true );
80
+ };
81
+ const handleJsChange = ( value ) => {
82
+ setEditedJs( value );
83
+ setIsDirty( true );
84
+ };
85
+ const handleUpdate = () => {
86
+ setAttributes( {
87
+ content: serializeContent( {
88
+ html: editedHtml,
89
+ css: editedCss,
90
+ js: editedJs,
91
+ } ),
92
+ } );
93
+ setIsDirty( false );
94
+ };
95
+ const handleCancel = () => {
96
+ setIsDirty( false );
97
+ onRequestClose();
98
+ };
99
+ const handleRequestClose = () => {
100
+ if ( isDirty ) {
101
+ setShowUnsavedWarning( true );
102
+ } else {
103
+ onRequestClose();
104
+ }
105
+ };
106
+ const handleDiscardChanges = () => {
107
+ setShowUnsavedWarning( false );
108
+ onRequestClose();
109
+ };
110
+ const handleContinueEditing = () => {
111
+ setShowUnsavedWarning( false );
112
+ };
113
+ const handleUpdateAndClose = () => {
114
+ handleUpdate();
115
+ onRequestClose();
116
+ };
117
+ const toggleFullscreen = () => {
118
+ setIsFullscreen( ( prevState ) => ! prevState );
119
+ };
120
+
121
+ return (
122
+ <>
123
+ <Modal
124
+ title={ __( 'Edit HTML' ) }
125
+ onRequestClose={ handleRequestClose }
126
+ className="block-library-html__modal"
127
+ size="large"
128
+ isDismissible={ false }
129
+ shouldCloseOnClickOutside={ ! isDirty }
130
+ shouldCloseOnEsc={ ! isDirty }
131
+ isFullScreen={ isFullscreen }
132
+ __experimentalHideHeader
133
+ >
134
+ { styleContent && (
135
+ <style
136
+ dangerouslySetInnerHTML={ { __html: styleContent } }
137
+ />
138
+ ) }
139
+ <Tabs orientation="horizontal" defaultTabId="html">
140
+ <VStack spacing={ 4 } style={ { height: '100%' } }>
141
+ <HStack justify="space-between">
142
+ <div>
143
+ <Tabs.TabList>
144
+ <Tabs.Tab tabId="html">HTML</Tabs.Tab>
145
+ <Tabs.Tab tabId="css">CSS</Tabs.Tab>
146
+ { shouldShowJsTab && (
147
+ <Tabs.Tab tabId="js">
148
+ { __( 'JavaScript' ) }
149
+ </Tabs.Tab>
150
+ ) }
151
+ </Tabs.TabList>
152
+ </div>
153
+ <div>
154
+ <Button
155
+ __next40pxDefaultSize
156
+ icon={ isFullscreen ? square : fullscreen }
157
+ label={ __( 'Enable/disable fullscreen' ) }
158
+ onClick={ toggleFullscreen }
159
+ variant="tertiary"
160
+ />
161
+ </div>
162
+ </HStack>
163
+ <HStack
164
+ alignment="stretch"
165
+ justify="flex-start"
166
+ spacing={ 4 }
167
+ className="block-library-html__modal-tabs"
168
+ style={ { flexGrow: 1 } }
169
+ >
170
+ <div style={ { flexGrow: 1 } }>
171
+ <Tabs.TabPanel
172
+ tabId="html"
173
+ focusable={ false }
174
+ className="block-library-html__modal-tab"
175
+ >
176
+ <PlainText
177
+ value={ editedHtml }
178
+ onChange={ handleHtmlChange }
179
+ placeholder={ __( 'Write HTML…' ) }
180
+ aria-label={ __( 'HTML' ) }
181
+ className="block-library-html__modal-editor"
182
+ />
183
+ </Tabs.TabPanel>
184
+ <Tabs.TabPanel
185
+ tabId="css"
186
+ focusable={ false }
187
+ className="block-library-html__modal-tab"
188
+ >
189
+ <PlainText
190
+ value={ editedCss }
191
+ onChange={ handleCssChange }
192
+ placeholder={ __( 'Write CSS…' ) }
193
+ aria-label={ __( 'CSS' ) }
194
+ className="block-library-html__modal-editor"
195
+ />
196
+ </Tabs.TabPanel>
197
+ { shouldShowJsTab && (
198
+ <Tabs.TabPanel
199
+ tabId="js"
200
+ focusable={ false }
201
+ className="block-library-html__modal-tab"
202
+ >
203
+ <PlainText
204
+ value={ editedJs }
205
+ onChange={ handleJsChange }
206
+ placeholder={ __(
207
+ 'Write JavaScript…'
208
+ ) }
209
+ aria-label={ __( 'JavaScript' ) }
210
+ className="block-library-html__modal-editor"
211
+ />
212
+ </Tabs.TabPanel>
213
+ ) }
214
+ </div>
215
+ <div
216
+ className="block-library-html__preview"
217
+ style={ { width: '50%' } }
218
+ >
219
+ <Preview
220
+ content={ serializeContent( {
221
+ html: editedHtml,
222
+ css: editedCss,
223
+ js: editedJs,
224
+ } ) }
225
+ />
226
+ </div>
227
+ </HStack>
228
+ <HStack
229
+ alignment="center"
230
+ justify="flex-end"
231
+ spacing={ 4 }
232
+ >
233
+ <Button
234
+ __next40pxDefaultSize
235
+ variant="tertiary"
236
+ onClick={ handleCancel }
237
+ >
238
+ { __( 'Cancel' ) }
239
+ </Button>
240
+ <Button
241
+ __next40pxDefaultSize
242
+ variant="primary"
243
+ onClick={ handleUpdateAndClose }
244
+ >
245
+ { __( 'Update' ) }
246
+ </Button>
247
+ </HStack>
248
+ </VStack>
249
+ </Tabs>
250
+ </Modal>
251
+
252
+ { showUnsavedWarning && (
253
+ <Modal
254
+ title={ __( 'Unsaved changes' ) }
255
+ onRequestClose={ handleContinueEditing }
256
+ size="medium"
257
+ >
258
+ <p>
259
+ { __(
260
+ 'You have unsaved changes. What would you like to do?'
261
+ ) }
262
+ </p>
263
+ <Flex direction="row" justify="flex-end" gap={ 2 }>
264
+ <Button
265
+ __next40pxDefaultSize
266
+ variant="secondary"
267
+ onClick={ handleDiscardChanges }
268
+ >
269
+ { __( 'Discard unsaved changes' ) }
270
+ </Button>
271
+ <Button
272
+ __next40pxDefaultSize
273
+ variant="secondary"
274
+ onClick={ handleContinueEditing }
275
+ >
276
+ { __( 'Continue editing' ) }
277
+ </Button>
278
+ <Button
279
+ __next40pxDefaultSize
280
+ variant="primary"
281
+ onClick={ handleUpdateAndClose }
282
+ >
283
+ { __( 'Update and close' ) }
284
+ </Button>
285
+ </Flex>
286
+ </Modal>
287
+ ) }
288
+ </>
289
+ );
290
+ }
@@ -0,0 +1,234 @@
1
+ /**
2
+ * Internal dependencies
3
+ */
4
+ import { parseContent, serializeContent } from '../utils';
5
+
6
+ describe( 'core/html', () => {
7
+ describe( 'parseContent()', () => {
8
+ it( 'should parse empty content', () => {
9
+ const result = parseContent( '' );
10
+ expect( result ).toEqual( { html: '', css: '', js: '' } );
11
+ } );
12
+
13
+ it( 'should parse whitespace-only content', () => {
14
+ const result = parseContent( ' \n\t ' );
15
+ expect( result ).toEqual( { html: '', css: '', js: '' } );
16
+ } );
17
+
18
+ it( 'should parse HTML-only content', () => {
19
+ const content = '<p>Hello World</p>';
20
+ const result = parseContent( content );
21
+ expect( result ).toEqual( {
22
+ html: '<p>Hello World</p>',
23
+ css: '',
24
+ js: '',
25
+ } );
26
+ } );
27
+
28
+ it( 'should parse CSS-only content', () => {
29
+ const content =
30
+ '<style data-wp-block-html="css">body { color: red; }</style>';
31
+ const result = parseContent( content );
32
+ expect( result ).toEqual( {
33
+ html: '',
34
+ css: 'body { color: red; }',
35
+ js: '',
36
+ } );
37
+ } );
38
+
39
+ it( 'should parse JavaScript-only content', () => {
40
+ const content =
41
+ '<script data-wp-block-html="js">console.log("hello");</script>';
42
+ const result = parseContent( content );
43
+ expect( result ).toEqual( {
44
+ html: '',
45
+ css: '',
46
+ js: 'console.log("hello");',
47
+ } );
48
+ } );
49
+
50
+ it( 'should parse content with all three sections', () => {
51
+ const content = `<style data-wp-block-html="css">
52
+ body { color: red; }
53
+ </style>
54
+
55
+ <script data-wp-block-html="js">
56
+ console.log("hello");
57
+ </script>
58
+
59
+ <p>Hello World</p>`;
60
+ const result = parseContent( content );
61
+ expect( result.css ).toBe( 'body { color: red; }' );
62
+ expect( result.js ).toBe( 'console.log("hello");' );
63
+ expect( result.html ).toBe( '<p>Hello World</p>' );
64
+ } );
65
+
66
+ it( 'should ignore unmarked style tags', () => {
67
+ const content = `<style>body { color: blue; }</style>
68
+ <style data-wp-block-html="css">body { color: red; }</style>
69
+ <p>Test</p>`;
70
+ const result = parseContent( content );
71
+ expect( result.css ).toBe( 'body { color: red; }' );
72
+ expect( result.html ).toContain(
73
+ '<style>body { color: blue; }</style>'
74
+ );
75
+ } );
76
+
77
+ it( 'should ignore unmarked script tags', () => {
78
+ const content = `<script>alert("unmarked");</script>
79
+ <script data-wp-block-html="js">console.log("marked");</script>
80
+ <p>Test</p>`;
81
+ const result = parseContent( content );
82
+ expect( result.js ).toBe( 'console.log("marked");' );
83
+ expect( result.html ).toContain(
84
+ '<script>alert("unmarked");</script>'
85
+ );
86
+ } );
87
+
88
+ it( 'should handle multiple marked style tags (takes first)', () => {
89
+ const content = `<style data-wp-block-html="css">first</style>
90
+ <style data-wp-block-html="css">second</style>`;
91
+ const result = parseContent( content );
92
+ expect( result.css ).toBe( 'first' );
93
+ expect( result.html ).toContain( 'second' );
94
+ } );
95
+
96
+ it( 'should handle multiple marked script tags (takes first)', () => {
97
+ const content = `<script data-wp-block-html="js">first</script>
98
+ <script data-wp-block-html="js">second</script>`;
99
+ const result = parseContent( content );
100
+ expect( result.js ).toBe( 'first' );
101
+ expect( result.html ).toContain( 'second' );
102
+ } );
103
+
104
+ it( 'should trim whitespace from extracted sections', () => {
105
+ const content = `<style data-wp-block-html="css">
106
+
107
+ body { color: red; }
108
+
109
+ </style>
110
+
111
+ <script data-wp-block-html="js">
112
+
113
+ console.log("test");
114
+
115
+ </script>
116
+
117
+ <p>Test</p> `;
118
+ const result = parseContent( content );
119
+ expect( result.css ).toBe( 'body { color: red; }' );
120
+ expect( result.js ).toBe( 'console.log("test");' );
121
+ expect( result.html ).toBe( '<p>Test</p>' );
122
+ } );
123
+
124
+ it( 'should handle malformed HTML gracefully', () => {
125
+ const content = '<p>Unclosed tag<div>Test</p>';
126
+ const result = parseContent( content );
127
+ expect( result ).toHaveProperty( 'html' );
128
+ expect( result ).toHaveProperty( 'css' );
129
+ expect( result ).toHaveProperty( 'js' );
130
+ } );
131
+ } );
132
+
133
+ describe( 'serializeContent()', () => {
134
+ it( 'should serialize empty sections', () => {
135
+ const result = serializeContent( { html: '', css: '', js: '' } );
136
+ expect( result ).toBe( '' );
137
+ } );
138
+
139
+ it( 'should serialize HTML-only', () => {
140
+ const result = serializeContent( {
141
+ html: '<p>Hello World</p>',
142
+ css: '',
143
+ js: '',
144
+ } );
145
+ expect( result ).toBe( '<p>Hello World</p>' );
146
+ } );
147
+
148
+ it( 'should serialize CSS-only', () => {
149
+ const result = serializeContent( {
150
+ html: '',
151
+ css: 'body { color: red; }',
152
+ js: '',
153
+ } );
154
+ expect( result ).toBe(
155
+ '<style data-wp-block-html="css">\nbody { color: red; }\n</style>'
156
+ );
157
+ } );
158
+
159
+ it( 'should serialize JavaScript-only', () => {
160
+ const result = serializeContent( {
161
+ html: '',
162
+ css: '',
163
+ js: 'console.log("test");',
164
+ } );
165
+ expect( result ).toBe(
166
+ '<script data-wp-block-html="js">\nconsole.log("test");\n</script>'
167
+ );
168
+ } );
169
+
170
+ it( 'should serialize all three sections in correct order', () => {
171
+ const result = serializeContent( {
172
+ html: '<p>Hello</p>',
173
+ css: 'body { color: red; }',
174
+ js: 'console.log("test");',
175
+ } );
176
+ expect( result ).toBe(
177
+ '<style data-wp-block-html="css">\nbody { color: red; }\n</style>\n\n<script data-wp-block-html="js">\nconsole.log("test");\n</script>\n\n<p>Hello</p>'
178
+ );
179
+ } );
180
+
181
+ it( 'should ignore whitespace-only sections', () => {
182
+ const result = serializeContent( {
183
+ html: '<p>Test</p>',
184
+ css: ' \n ',
185
+ js: '\t\t',
186
+ } );
187
+ expect( result ).toBe( '<p>Test</p>' );
188
+ } );
189
+
190
+ it( 'should handle missing properties gracefully', () => {
191
+ const result = serializeContent( {} );
192
+ expect( result ).toBe( '' );
193
+ } );
194
+
195
+ it( 'should handle undefined values', () => {
196
+ const result = serializeContent( {
197
+ html: undefined,
198
+ css: undefined,
199
+ js: undefined,
200
+ } );
201
+ expect( result ).toBe( '' );
202
+ } );
203
+ } );
204
+
205
+ describe( 'round-trip serialization', () => {
206
+ it( 'should maintain HTML-only content', () => {
207
+ const original = '<p>Hello World</p>';
208
+ const parsed = parseContent( original );
209
+ const serialized = serializeContent( parsed );
210
+ expect( serialized ).toBe( original );
211
+ } );
212
+
213
+ it( 'should maintain content with all sections', () => {
214
+ const original =
215
+ '<style data-wp-block-html="css">\nbody { color: red; }\n</style>\n\n<script data-wp-block-html="js">\nconsole.log("test");\n</script>\n\n<p>Hello</p>';
216
+ const parsed = parseContent( original );
217
+ const serialized = serializeContent( parsed );
218
+ expect( serialized ).toBe( original );
219
+ } );
220
+
221
+ it( 'should maintain complex HTML structures', () => {
222
+ const sections = {
223
+ html: '<div><h1>Title</h1><p>Paragraph</p></div>',
224
+ css: 'h1 { font-size: 2em; }',
225
+ js: 'document.addEventListener("load", () => {});',
226
+ };
227
+ const serialized = serializeContent( sections );
228
+ const parsed = parseContent( serialized );
229
+ expect( parsed.html ).toBe( sections.html );
230
+ expect( parsed.css ).toBe( sections.css );
231
+ expect( parsed.js ).toBe( sections.js );
232
+ } );
233
+ } );
234
+ } );
@@ -0,0 +1,75 @@
1
+ /**
2
+ * Parses content string into separate HTML, CSS, and JS sections.
3
+ *
4
+ * Extracts CSS from <style data-wp-block-html="css"> tags and
5
+ * JavaScript from <script data-wp-block-html="js"> tags.
6
+ * Everything else is treated as HTML.
7
+ *
8
+ * @param {string} content - The combined content string
9
+ * @return {Object} Object with html, css, and js properties
10
+ */
11
+ export function parseContent( content = '' ) {
12
+ if ( ! content || ! content.trim() ) {
13
+ return { html: '', css: '', js: '' };
14
+ }
15
+
16
+ // Create a temporary document to parse HTML safely
17
+ const doc = document.implementation.createHTMLDocument( '' );
18
+ doc.body.innerHTML = content;
19
+
20
+ // Extract CSS from marked style tag
21
+ const styleTag = doc.body.querySelector(
22
+ 'style[data-wp-block-html="css"]'
23
+ );
24
+ const css = styleTag ? styleTag.textContent.trim() : '';
25
+ if ( styleTag ) {
26
+ styleTag.remove();
27
+ }
28
+
29
+ // Extract JS from marked script tag
30
+ const scriptTag = doc.body.querySelector(
31
+ 'script[data-wp-block-html="js"]'
32
+ );
33
+ const js = scriptTag ? scriptTag.textContent.trim() : '';
34
+ if ( scriptTag ) {
35
+ scriptTag.remove();
36
+ }
37
+
38
+ // Everything else is HTML
39
+ const html = doc.body.innerHTML.trim();
40
+
41
+ return { html, css, js };
42
+ }
43
+
44
+ /**
45
+ * Serializes HTML, CSS, and JS into a single content string.
46
+ *
47
+ * Creates marked <style> and <script> tags for CSS and JS sections,
48
+ * then appends the HTML content.
49
+ *
50
+ * @param {Object} sections Object with html, css, and js properties
51
+ * @param {string} sections.html HTML content
52
+ * @param {string} sections.css CSS content
53
+ * @param {string} sections.js JavaScript content
54
+ * @return {string} Combined content string
55
+ */
56
+ export function serializeContent( { html = '', css = '', js = '' } ) {
57
+ const parts = [];
58
+
59
+ // Add CSS if present
60
+ if ( css.trim() ) {
61
+ parts.push( `<style data-wp-block-html="css">\n${ css }\n</style>` );
62
+ }
63
+
64
+ // Add JS if present
65
+ if ( js.trim() ) {
66
+ parts.push( `<script data-wp-block-html="js">\n${ js }\n</script>` );
67
+ }
68
+
69
+ // Add HTML
70
+ if ( html.trim() ) {
71
+ parts.push( html );
72
+ }
73
+
74
+ return parts.join( '\n\n' );
75
+ }