Skip to content

Commit 0666463

Browse files
authored
Merge pull request #3727 from gambitph/fix/yoast-seo-highlighting-2422
fix: Yoast SEO text highlighting for Stackable blocks
2 parents fcc43c4 + 46b0623 commit 0666463

4 files changed

Lines changed: 998 additions & 0 deletions

File tree

‎src/compatibility/index.js‎

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
11
import './kadence-theme'
22
import './wp-6-2'
33
import './wp-pre-7'
4+
import './yoast-seo'
Lines changed: 355 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,355 @@
1+
/**
2+
* Internal dependencies
3+
*/
4+
import {
5+
STACKABLE_ANNOTATABLE_BLOCKS,
6+
FIELD_TO_STACKABLE_BLOCKS,
7+
getFieldsToMark,
8+
shouldAnnotateBlock,
9+
getYoastmarkOffsets,
10+
calculateAnnotationsForTextFormat,
11+
createAnnotationsFromPositionBasedMarks,
12+
getAnnotationsForStackableBlock,
13+
getAnnotationsForStackableBlocks,
14+
} from '../helpers'
15+
16+
/**
17+
* Creates a mock Yoast Mark object for search-based highlighting.
18+
*
19+
* @param {string} original Sentence without yoastmark tags.
20+
* @param {string} marked Sentence with yoastmark tags.
21+
* @param {Array} fieldsToMark Optional fields to mark.
22+
*
23+
* @return {Object} Mock mark.
24+
*/
25+
const createSearchMark = ( original, marked, fieldsToMark = [] ) => ( {
26+
getOriginal: () => original,
27+
getMarked: () => marked,
28+
getFieldsToMark: () => fieldsToMark,
29+
hasBlockPosition: () => false,
30+
} )
31+
32+
/**
33+
* Creates a mock Yoast Mark object for position-based highlighting.
34+
*
35+
* @param {Object} options Mark options.
36+
* @param {string} options.clientId Block client ID.
37+
* @param {number} options.startOffsetBlock Block start offset.
38+
* @param {number} options.endOffsetBlock Block end offset.
39+
* @param {string} [options.original] Original text.
40+
* @param {string} [options.marked] Marked text.
41+
* @param {Array} [options.fieldsToMark] Fields to mark.
42+
*
43+
* @return {Object} Mock mark.
44+
*/
45+
const createPositionMark = ( {
46+
clientId,
47+
startOffsetBlock,
48+
endOffsetBlock,
49+
original = '',
50+
marked = '',
51+
fieldsToMark = [],
52+
} ) => ( {
53+
getOriginal: () => original,
54+
getMarked: () => marked,
55+
getFieldsToMark: () => fieldsToMark,
56+
hasBlockPosition: () => true,
57+
getBlockClientId: () => clientId,
58+
getBlockPositionStart: () => startOffsetBlock,
59+
getBlockPositionEnd: () => endOffsetBlock,
60+
} )
61+
62+
const createBlock = ( name, clientId, attributes, innerBlocks = [] ) => ( {
63+
name,
64+
clientId,
65+
attributes,
66+
innerBlocks,
67+
} )
68+
69+
describe( 'Yoast SEO compatibility helpers', () => {
70+
describe( 'STACKABLE_ANNOTATABLE_BLOCKS', () => {
71+
it( 'should map image blocks to figcaptionText', () => {
72+
expect( STACKABLE_ANNOTATABLE_BLOCKS[ 'stackable/image' ] ).toEqual( {
73+
attributeKey: 'figcaptionText',
74+
richTextIdentifier: 'text',
75+
} )
76+
} )
77+
78+
it( 'should map text-based blocks to the text attribute', () => {
79+
[ 'stackable/text', 'stackable/heading', 'stackable/subtitle', 'stackable/icon-list-item', 'stackable/button' ].forEach( blockName => {
80+
expect( STACKABLE_ANNOTATABLE_BLOCKS[ blockName ] ).toEqual( {
81+
attributeKey: 'text',
82+
richTextIdentifier: 'text',
83+
} )
84+
} )
85+
} )
86+
} )
87+
88+
describe( 'FIELD_TO_STACKABLE_BLOCKS', () => {
89+
it( 'should map field-specific assessments to the expected blocks', () => {
90+
expect( FIELD_TO_STACKABLE_BLOCKS.heading ).toEqual( [ 'stackable/heading', 'stackable/subtitle' ] )
91+
expect( FIELD_TO_STACKABLE_BLOCKS.paragraph ).toEqual( [ 'stackable/text', 'stackable/icon-list-item' ] )
92+
expect( FIELD_TO_STACKABLE_BLOCKS.caption ).toEqual( [ 'stackable/image' ] )
93+
} )
94+
95+
it( 'should allow button blocks through when fieldsToMark is empty', () => {
96+
expect( shouldAnnotateBlock( { name: 'stackable/button' }, [] ) ).toBe( true )
97+
expect( FIELD_TO_STACKABLE_BLOCKS.paragraph ).not.toContain( 'stackable/button' )
98+
} )
99+
} )
100+
101+
describe( 'getFieldsToMark', () => {
102+
it( 'should collect unique fields from marks', () => {
103+
const marks = [
104+
createSearchMark( 'a', 'a', [ 'heading' ] ),
105+
createSearchMark( 'b', 'b', [ 'paragraph', 'heading' ] ),
106+
]
107+
108+
expect( getFieldsToMark( marks ) ).toEqual( [ 'heading', 'paragraph' ] )
109+
} )
110+
} )
111+
112+
describe( 'shouldAnnotateBlock', () => {
113+
const textBlock = { name: 'stackable/text' }
114+
const headingBlock = { name: 'stackable/heading' }
115+
const imageBlock = { name: 'stackable/image' }
116+
const buttonBlock = { name: 'stackable/button' }
117+
118+
it( 'should annotate all supported blocks when fieldsToMark is empty', () => {
119+
expect( shouldAnnotateBlock( textBlock, [] ) ).toBe( true )
120+
expect( shouldAnnotateBlock( headingBlock, [] ) ).toBe( true )
121+
expect( shouldAnnotateBlock( imageBlock, [] ) ).toBe( true )
122+
expect( shouldAnnotateBlock( buttonBlock, [] ) ).toBe( true )
123+
} )
124+
125+
it( 'should only annotate heading blocks for heading fields', () => {
126+
expect( shouldAnnotateBlock( headingBlock, [ 'heading' ] ) ).toBe( true )
127+
expect( shouldAnnotateBlock( { name: 'stackable/subtitle' }, [ 'heading' ] ) ).toBe( true )
128+
expect( shouldAnnotateBlock( textBlock, [ 'heading' ] ) ).toBe( false )
129+
} )
130+
131+
it( 'should only annotate paragraph blocks for paragraph fields', () => {
132+
expect( shouldAnnotateBlock( textBlock, [ 'paragraph' ] ) ).toBe( true )
133+
expect( shouldAnnotateBlock( { name: 'stackable/icon-list-item' }, [ 'paragraph' ] ) ).toBe( true )
134+
expect( shouldAnnotateBlock( headingBlock, [ 'paragraph' ] ) ).toBe( false )
135+
} )
136+
137+
it( 'should only annotate image blocks for caption fields', () => {
138+
expect( shouldAnnotateBlock( imageBlock, [ 'caption' ] ) ).toBe( true )
139+
expect( shouldAnnotateBlock( textBlock, [ 'caption' ] ) ).toBe( false )
140+
} )
141+
142+
it( 'should ignore unsupported blocks', () => {
143+
expect( shouldAnnotateBlock( { name: 'stackable/columns' }, [] ) ).toBe( false )
144+
} )
145+
} )
146+
147+
describe( 'getYoastmarkOffsets', () => {
148+
it( 'should extract offsets from single-quoted yoastmark tags', () => {
149+
const marked = "Next, the field <yoastmark class='yoast-text-mark'>was selected</yoastmark> by the team."
150+
151+
expect( getYoastmarkOffsets( marked ) ).toEqual( [ {
152+
startOffset: 16,
153+
endOffset: 28,
154+
} ] )
155+
} )
156+
} )
157+
158+
describe( 'calculateAnnotationsForTextFormat', () => {
159+
it( 'should calculate offsets for passive voice style marks', () => {
160+
const sentence = 'Next, the field that you want to fetch data from was selected.'
161+
const mark = createSearchMark(
162+
sentence,
163+
"Next, the field that you want to fetch data from <yoastmark class='yoast-text-mark'>was selected</yoastmark>."
164+
)
165+
166+
expect( calculateAnnotationsForTextFormat( sentence, mark ) ).toEqual( [ {
167+
startOffset: 49,
168+
endOffset: 61,
169+
} ] )
170+
} )
171+
172+
it( 'should return an empty array when the sentence is not found', () => {
173+
const mark = createSearchMark(
174+
'Missing sentence.',
175+
"<yoastmark class='yoast-text-mark'>Missing sentence.</yoastmark>"
176+
)
177+
178+
expect( calculateAnnotationsForTextFormat( 'Different content.', mark ) ).toEqual( [] )
179+
} )
180+
} )
181+
182+
describe( 'createAnnotationsFromPositionBasedMarks', () => {
183+
it( 'should return annotations for matching client IDs', () => {
184+
const mark = createPositionMark( {
185+
clientId: 'block-1',
186+
startOffsetBlock: 0,
187+
endOffsetBlock: 11,
188+
} )
189+
190+
expect(
191+
createAnnotationsFromPositionBasedMarks(
192+
mark,
193+
'block-1',
194+
'Giant panda',
195+
'Giant panda'
196+
)
197+
).toEqual( [ {
198+
startOffset: 0,
199+
endOffset: 11,
200+
} ] )
201+
} )
202+
203+
it( 'should return an empty array for non-matching client IDs', () => {
204+
const mark = createPositionMark( {
205+
clientId: 'block-1',
206+
startOffsetBlock: 0,
207+
endOffsetBlock: 11,
208+
} )
209+
210+
expect(
211+
createAnnotationsFromPositionBasedMarks(
212+
mark,
213+
'block-2',
214+
'Giant panda',
215+
'Giant panda'
216+
)
217+
).toEqual( [] )
218+
} )
219+
220+
it( 'should adjust offsets when HTML tags are present', () => {
221+
const mark = createPositionMark( {
222+
clientId: 'block-1',
223+
startOffsetBlock: 16,
224+
endOffsetBlock: 33,
225+
} )
226+
227+
expect(
228+
createAnnotationsFromPositionBasedMarks(
229+
mark,
230+
'block-1',
231+
'This is a giant <strong>panda</strong>.',
232+
'This is a giant panda.'
233+
)
234+
).toEqual( [ {
235+
startOffset: 16,
236+
endOffset: 25,
237+
} ] )
238+
} )
239+
} )
240+
241+
describe( 'getAnnotationsForStackableBlock', () => {
242+
it( 'should annotate stackable/text blocks', () => {
243+
const sentence = 'The report was written by the team.'
244+
const block = createBlock( 'stackable/text', 'text-1', {
245+
text: sentence,
246+
} )
247+
const mark = createSearchMark(
248+
sentence,
249+
"The report <yoastmark class='yoast-text-mark'>was written by</yoastmark> the team."
250+
)
251+
252+
expect( getAnnotationsForStackableBlock( block, [ mark ] ) ).toEqual( [ {
253+
startOffset: 11,
254+
endOffset: 25,
255+
block: 'text-1',
256+
richTextIdentifier: 'text',
257+
} ] )
258+
} )
259+
260+
it( 'should annotate stackable/image figcaption text', () => {
261+
const caption = 'A photo was taken by the team.'
262+
const block = createBlock( 'stackable/image', 'image-1', {
263+
figcaptionText: caption,
264+
} )
265+
const mark = createSearchMark(
266+
caption,
267+
"A photo <yoastmark class='yoast-text-mark'>was taken by</yoastmark> the team."
268+
)
269+
270+
expect( getAnnotationsForStackableBlock( block, [ mark ] ) ).toEqual( [ {
271+
startOffset: 8,
272+
endOffset: 20,
273+
block: 'image-1',
274+
richTextIdentifier: 'text',
275+
} ] )
276+
} )
277+
278+
it( 'should annotate stackable/button text', () => {
279+
const buttonText = 'Click here to get started.'
280+
const block = createBlock( 'stackable/button', 'button-1', {
281+
text: buttonText,
282+
} )
283+
const mark = createSearchMark(
284+
buttonText,
285+
"Click <yoastmark class='yoast-text-mark'>here</yoastmark> to get started."
286+
)
287+
288+
expect( getAnnotationsForStackableBlock( block, [ mark ] ) ).toEqual( [ {
289+
startOffset: 6,
290+
endOffset: 10,
291+
block: 'button-1',
292+
richTextIdentifier: 'text',
293+
} ] )
294+
} )
295+
} )
296+
297+
describe( 'getAnnotationsForStackableBlocks', () => {
298+
it( 'should recurse into inner blocks', () => {
299+
const sentence = 'The report was written by the team.'
300+
const innerBlock = createBlock( 'stackable/text', 'inner-text-1', {
301+
text: sentence,
302+
} )
303+
const blocks = [
304+
createBlock( 'stackable/columns', 'columns-1', {}, [ innerBlock ] ),
305+
]
306+
const mark = createSearchMark(
307+
sentence,
308+
"The report <yoastmark class='yoast-text-mark'>was written by</yoastmark> the team."
309+
)
310+
311+
expect( getAnnotationsForStackableBlocks( blocks, [ mark ], [] ) ).toEqual( [ {
312+
startOffset: 11,
313+
endOffset: 25,
314+
block: 'inner-text-1',
315+
richTextIdentifier: 'text',
316+
} ] )
317+
} )
318+
319+
it( 'should respect fieldsToMark when filtering blocks', () => {
320+
const textBlock = createBlock( 'stackable/text', 'text-1', {
321+
text: 'Paragraph content.',
322+
} )
323+
const headingBlock = createBlock( 'stackable/heading', 'heading-1', {
324+
text: 'Heading content.',
325+
} )
326+
const textMark = createSearchMark(
327+
'Paragraph content.',
328+
"<yoastmark class='yoast-text-mark'>Paragraph content.</yoastmark>",
329+
[ 'paragraph' ]
330+
)
331+
const headingMark = createSearchMark(
332+
'Heading content.',
333+
"<yoastmark class='yoast-text-mark'>Heading content.</yoastmark>",
334+
[ 'heading' ]
335+
)
336+
337+
const paragraphAnnotations = getAnnotationsForStackableBlocks(
338+
[ textBlock, headingBlock ],
339+
[ textMark ],
340+
[ 'paragraph' ]
341+
)
342+
const headingAnnotations = getAnnotationsForStackableBlocks(
343+
[ textBlock, headingBlock ],
344+
[ headingMark ],
345+
[ 'heading' ]
346+
)
347+
348+
expect( paragraphAnnotations ).toHaveLength( 1 )
349+
expect( paragraphAnnotations[ 0 ].block ).toBe( 'text-1' )
350+
351+
expect( headingAnnotations ).toHaveLength( 1 )
352+
expect( headingAnnotations[ 0 ].block ).toBe( 'heading-1' )
353+
} )
354+
} )
355+
} )

0 commit comments

Comments
 (0)