@@ -10,15 +10,14 @@ import {
1010 buildLastModifiedClause ,
1111 confluenceConnector ,
1212 confluenceStorageToPlainText ,
13+ confluenceViewToPlainText ,
1314 DYNAMIC_CONTENT_SKIP_REASON ,
1415 escapeCql ,
1516 extractConfluenceStorageText ,
1617 isCurrentContent ,
17- preserveConfluenceCallouts ,
1818 readIncludedLabels ,
1919} from '@/connectors/confluence/confluence'
2020import { extractCursor } from '@/connectors/confluence/cursor'
21- import { htmlToPlainText } from '@/connectors/utils'
2221
2322describe ( 'Confluence service-account scopes' , ( ) => {
2423 it ( 'requests metadata and role reads needed for complete mirrored ACLs' , ( ) => {
@@ -216,14 +215,14 @@ describe('readIncludedLabels', () => {
216215 } )
217216} )
218217
219- describe ( 'preserveConfluenceCallouts ' , ( ) => {
218+ describe ( 'confluenceViewToPlainText ' , ( ) => {
220219 it . concurrent ( 'handles empty content' , ( ) => {
221- expect ( preserveConfluenceCallouts ( '' ) ) . toBe ( '' )
220+ expect ( confluenceViewToPlainText ( '' ) ) . toBe ( '' )
222221 } )
223222
224223 it . concurrent ( 'leaves content with no macros unchanged' , ( ) => {
225224 const html = '<p>Just a normal paragraph.</p>'
226- expect ( preserveConfluenceCallouts ( html ) ) . toContain ( 'Just a normal paragraph.' )
225+ expect ( confluenceViewToPlainText ( html ) ) . toContain ( 'Just a normal paragraph.' )
227226 } )
228227
229228 it . concurrent ( 'labels a built-in warning macro and keeps its body' , ( ) => {
@@ -232,7 +231,7 @@ describe('preserveConfluenceCallouts', () => {
232231 '<span class="aui-icon aui-icon-small aui-iconfont-warning confluence-information-macro-icon"></span>' +
233232 '<div class="confluence-information-macro-body"><p>Do NOT use this form for GitLab access.</p></div>' +
234233 '</div>'
235- const result = preserveConfluenceCallouts ( html )
234+ const result = confluenceViewToPlainText ( html )
236235 expect ( result ) . toContain ( '[WARNING]' )
237236 expect ( result ) . toContain ( 'Do NOT use this form for GitLab access.' )
238237 } )
@@ -242,23 +241,23 @@ describe('preserveConfluenceCallouts', () => {
242241 '<div class="confluence-information-macro confluence-information-macro-information">' +
243242 '<div class="confluence-information-macro-body"><p>Heads up.</p></div>' +
244243 '</div>'
245- expect ( preserveConfluenceCallouts ( html ) ) . toContain ( '[INFO] Heads up.' )
244+ expect ( confluenceViewToPlainText ( html ) ) . toContain ( '[INFO] Heads up.' )
246245 } )
247246
248247 it . concurrent ( 'labels a built-in note macro' , ( ) => {
249248 const html =
250249 '<div class="confluence-information-macro confluence-information-macro-note">' +
251250 '<div class="confluence-information-macro-body"><p>See also.</p></div>' +
252251 '</div>'
253- expect ( preserveConfluenceCallouts ( html ) ) . toContain ( '[NOTE] See also.' )
252+ expect ( confluenceViewToPlainText ( html ) ) . toContain ( '[NOTE] See also.' )
254253 } )
255254
256255 it . concurrent ( 'labels a built-in tip macro' , ( ) => {
257256 const html =
258257 '<div class="confluence-information-macro confluence-information-macro-tip">' +
259258 '<div class="confluence-information-macro-body"><p>Pro tip.</p></div>' +
260259 '</div>'
261- expect ( preserveConfluenceCallouts ( html ) ) . toContain ( '[TIP] Pro tip.' )
260+ expect ( confluenceViewToPlainText ( html ) ) . toContain ( '[TIP] Pro tip.' )
262261 } )
263262
264263 it . concurrent ( 'labels a generic custom-colored Panel macro using its header title' , ( ) => {
@@ -267,7 +266,7 @@ describe('preserveConfluenceCallouts', () => {
267266 '<div class="panelHeader" style="background-color: #ffebe6;"><b>Do NOT use this form for:</b></div>' +
268267 '<div class="panelContent"><p>GitLab access requests go to the private channel instead.</p></div>' +
269268 '</div>'
270- const result = preserveConfluenceCallouts ( html )
269+ const result = confluenceViewToPlainText ( html )
271270 expect ( result ) . toContain ( '[CALLOUT: Do NOT use this form for:]' )
272271 expect ( result ) . toContain ( 'GitLab access requests go to the private channel instead.' )
273272 } )
@@ -276,7 +275,7 @@ describe('preserveConfluenceCallouts', () => {
276275 const html =
277276 '<div class="panel"><div class="panelHeader"><b>Warning:</b></div>' +
278277 '<div class="panelContent"><p>See replacement form.</p></div></div>'
279- const result = preserveConfluenceCallouts ( html )
278+ const result = confluenceViewToPlainText ( html )
280279 expect ( result ) . toContain ( '[CALLOUT: Warning:] See replacement form.' )
281280 } )
282281
@@ -286,21 +285,21 @@ describe('preserveConfluenceCallouts', () => {
286285 const html =
287286 '<div class="panel"><div class="panelHeader"><b>Warning:</b> <span>Do not use</span></div>' +
288287 '<div class="panelContent"><p>See replacement form.</p></div></div>'
289- const result = preserveConfluenceCallouts ( html )
288+ const result = confluenceViewToPlainText ( html )
290289 expect ( result ) . toContain ( '[CALLOUT: Warning: Do not use]' )
291290 }
292291 )
293292
294293 it . concurrent ( 'falls back to a bare CALLOUT label when a Panel macro has no header text' , ( ) => {
295294 const html =
296295 '<div class="panel"><div class="panelContent"><p>Untitled panel body.</p></div></div>'
297- const result = preserveConfluenceCallouts ( html )
296+ const result = confluenceViewToPlainText ( html )
298297 expect ( result ) . toContain ( '[CALLOUT]' )
299298 expect ( result ) . toContain ( 'Untitled panel body.' )
300299 } )
301300
302301 it . concurrent (
303- 'keeps the exclusion marker attached to its content through htmlToPlainText, even across surrounding whitespace collapse' ,
302+ 'keeps the exclusion marker attached to its content across surrounding whitespace collapse' ,
304303 ( ) => {
305304 const html =
306305 '<p>Intro paragraph.</p>\n\n' +
@@ -309,7 +308,7 @@ describe('preserveConfluenceCallouts', () => {
309308 '<ul><li>GitLab</li></ul></div>' +
310309 '</div>\n\n' +
311310 '<p>Trailing paragraph.</p>'
312- const plainText = htmlToPlainText ( preserveConfluenceCallouts ( html ) )
311+ const plainText = confluenceViewToPlainText ( html )
313312 expect ( plainText ) . toContain ( '[WARNING] Do NOT use this form for: GitLab' )
314313 expect ( plainText ) . toContain ( 'Intro paragraph.' )
315314 expect ( plainText ) . toContain ( 'Trailing paragraph.' )
@@ -325,7 +324,7 @@ describe('preserveConfluenceCallouts', () => {
325324 '<p>Do NOT use this form for:</p>' +
326325 '<ul><li>GitLab</li><li>ServiceNow</li></ul>' +
327326 '</div></div>'
328- const result = preserveConfluenceCallouts ( html )
327+ const result = confluenceViewToPlainText ( html )
329328 expect ( result ) . not . toContain ( 'for:GitLab' )
330329 expect ( result ) . not . toContain ( 'GitLabServiceNow' )
331330 expect ( result ) . toContain ( 'Do NOT use this form for: GitLab ServiceNow' )
@@ -339,7 +338,7 @@ describe('preserveConfluenceCallouts', () => {
339338 '<div class="panel"><div class="panelContent">' +
340339 '<p>First sentence.</p><p>Second sentence.</p>' +
341340 '</div></div>'
342- const result = preserveConfluenceCallouts ( html )
341+ const result = confluenceViewToPlainText ( html )
343342 expect ( result ) . toContain ( 'First sentence. Second sentence.' )
344343 expect ( result ) . not . toContain ( 'sentence.Second' )
345344 }
@@ -355,7 +354,7 @@ describe('preserveConfluenceCallouts', () => {
355354 '<ul><li>Nested item A</li><li>Nested item B</li></ul>' +
356355 '</li><li>Outer item two</li></ul>' +
357356 '</div></div>'
358- const result = preserveConfluenceCallouts ( html )
357+ const result = confluenceViewToPlainText ( html )
359358 // Each nested <li>'s text must appear exactly once, not duplicated by the
360359 // outer <li> also being matched and its .text() recursing into it.
361360 const occurrences = ( result . match ( / N e s t e d i t e m A / g) ?? [ ] ) . length
@@ -370,7 +369,7 @@ describe('preserveConfluenceCallouts', () => {
370369 '<div class="panel"><div class="panelContent">' +
371370 '<table><tr><td>Cell text<blockquote><p>quoted text</p></blockquote>after quote</td></tr></table>' +
372371 '</div></div>'
373- const result = preserveConfluenceCallouts ( html )
372+ const result = confluenceViewToPlainText ( html )
374373 expect ( result ) . not . toContain ( 'quotedtext' )
375374 expect ( result ) . not . toContain ( 'textafter' )
376375 expect ( result ) . toContain ( 'Cell text quoted text after quote' )
@@ -383,7 +382,7 @@ describe('preserveConfluenceCallouts', () => {
383382 '<div class="panel"><div class="panelContent">' +
384383 '<p>This is un<b>believe</b>able.</p>' +
385384 '</div></div>'
386- const result = preserveConfluenceCallouts ( html )
385+ const result = confluenceViewToPlainText ( html )
387386 expect ( result ) . not . toContain ( 'un believe able' )
388387 expect ( result ) . toContain ( 'This is unbelieveable.' )
389388 }
@@ -394,7 +393,7 @@ describe('preserveConfluenceCallouts', () => {
394393 '<div class="confluence-information-macro confluence-information-macro-warning">' +
395394 '<div class="confluence-information-macro-body"><p>Do not proceed<b>!</b></p></div>' +
396395 '</div>'
397- const result = preserveConfluenceCallouts ( html )
396+ const result = confluenceViewToPlainText ( html )
398397 expect ( result ) . not . toContain ( 'proceed !' )
399398 expect ( result ) . toContain ( '[WARNING] Do not proceed!' )
400399 } )
@@ -404,7 +403,7 @@ describe('preserveConfluenceCallouts', () => {
404403 '<div class="panel"><div class="panelContent">' +
405404 '<p>Do <b>NOT</b> use this form.</p>' +
406405 '</div></div>'
407- const result = preserveConfluenceCallouts ( html )
406+ const result = confluenceViewToPlainText ( html )
408407 expect ( result ) . toContain ( 'Do NOT use this form.' )
409408 } )
410409
@@ -416,7 +415,7 @@ describe('preserveConfluenceCallouts', () => {
416415 '<div class="panel"><div class="panelHeader"><b>Inner</b></div>' +
417416 '<div class="panelContent"><p>inner body</p></div></div>' +
418417 '</div></div>'
419- const result = preserveConfluenceCallouts ( html )
418+ const result = confluenceViewToPlainText ( html )
420419 expect ( result ) . toContain ( '[CALLOUT: Outer]' )
421420 expect ( result ) . toContain ( '[CALLOUT: Inner] inner body' )
422421 }
@@ -430,7 +429,7 @@ describe('preserveConfluenceCallouts', () => {
430429 '<div class="confluence-information-macro confluence-information-macro-warning">' +
431430 '<div class="confluence-information-macro-body"><p>Do not use this.</p></div></div>' +
432431 '</div></div>'
433- const result = preserveConfluenceCallouts ( html )
432+ const result = confluenceViewToPlainText ( html )
434433 expect ( result ) . toContain ( '[WARNING] Do not use this.' )
435434 }
436435 )
@@ -443,7 +442,7 @@ describe('preserveConfluenceCallouts', () => {
443442 '<div class="panel"><div class="panelHeader"><b>Inner title</b></div>' +
444443 '<div class="panelContent"><p>inner body</p></div></div>' +
445444 '</div></div>'
446- const result = preserveConfluenceCallouts ( html )
445+ const result = confluenceViewToPlainText ( html )
447446 // The outer panel has no header of its own — it must fall back to a
448447 // bare [CALLOUT], not steal "Inner title" from the nested panel.
449448 expect ( result ) . toContain ( '[CALLOUT] [CALLOUT: Inner title] inner body' )
@@ -455,10 +454,98 @@ describe('preserveConfluenceCallouts', () => {
455454 '<div class="confluence-information-macro confluence-information-macro-warning">' +
456455 '<div class="confluence-information-macro-body"><p>Do NOT use this form for:<br>GitLab</p></div>' +
457456 '</div>'
458- const result = preserveConfluenceCallouts ( html )
457+ const result = confluenceViewToPlainText ( html )
459458 expect ( result ) . not . toContain ( 'for:GitLab' )
460459 expect ( result ) . toContain ( '[WARNING] Do NOT use this form for: GitLab' )
461460 } )
461+
462+ it . concurrent ( 'drops app macro bootstrap scripts, inline styles, and chart data' , ( ) => {
463+ const html =
464+ '<p><style>[data-colorid=yr9gnc4vid]{color:#333333}</style>' +
465+ '<span data-colorid="yr9gnc4vid">Colored text</span></p>' +
466+ '<style type="text/css">/*<![CDATA[*/ div.rbtoc1748352890217 {padding: 0px;} /*]]>*/</style>' +
467+ '<div class="ap-container" id="ap-lucidchart"><div class="ap-content"></div>' +
468+ '<script class="ap-iframe-body-script">//<![CDATA[\n(function(){ var data = {"addon_key":"lucidchart-app"}; AP._createContainer(data); }());\n//]]></script></div>' +
469+ '<script class="chart-render-data" type="application/json">{"pluginKey": "confluence.extra.chart"}</script>' +
470+ '<p>After</p>'
471+ expect ( confluenceViewToPlainText ( html ) ) . toBe ( 'Colored text After' )
472+ } )
473+
474+ it . concurrent ( 'keeps the word break a dropped script or style occupied' , ( ) => {
475+ expect ( confluenceViewToPlainText ( '<p>Before<style>.a{}</style>After</p>' ) ) . toBe ( 'Before After' )
476+ } )
477+
478+ it . concurrent ( 'treats a page holding only an app macro as having no text' , ( ) => {
479+ const html =
480+ '<div class="ap-container"><div class="ap-content"></div>' +
481+ '<script class="ap-iframe-body-script">(function(){ var data = {"addon_key":"drawio"}; }());</script></div>'
482+ expect ( confluenceViewToPlainText ( html ) ) . toBe ( '' )
483+ } )
484+
485+ it . concurrent ( 'reduces an unresolved Jira issue macro to its issue key' , ( ) => {
486+ const html =
487+ '<p>Tracked in ' +
488+ '<span class="confluence-jim-macro jira-issue" data-jira-key="ENG-101">' +
489+ '<a href="https://example.atlassian.net/browse/ENG-101" class="jira-issue-key">' +
490+ '<span class="aui-icon aui-icon-wait issue-placeholder"></span>ENG-101</a> - ' +
491+ '<span class="summary">Getting issue details...</span> ' +
492+ '<span class="aui-lozenge aui-lozenge-subtle aui-lozenge-default issue-placeholder">STATUS</span>' +
493+ '</span> and ' +
494+ '<span class="confluence-jim-macro jira-issue conf-macro output-block">' +
495+ '<a href="https://example.atlassian.net/browse/ENG-102" class="jira-issue-key">' +
496+ '<span class="aui-icon aui-icon-wait issue-placeholder"> </span>ENG-102</a> - ' +
497+ '<span class="summary">이슈 세부사항 가져오는 중...</span> ' +
498+ '<span class="aui-lozenge aui-lozenge-subtle aui-lozenge-default issue-placeholder">상태</span>' +
499+ '</span>.</p>'
500+ expect ( confluenceViewToPlainText ( html ) ) . toBe ( 'Tracked in ENG-101 and ENG-102 .' )
501+ } )
502+
503+ it . concurrent ( 'falls back to the data attribute when the issue key link has no text' , ( ) => {
504+ const html =
505+ '<span class="confluence-jim-macro jira-issue" data-jira-key="ENG-103">' +
506+ '<span class="summary">Getting issue details...</span>' +
507+ '<span class="aui-lozenge issue-placeholder">STATUS</span></span>'
508+ expect ( confluenceViewToPlainText ( html ) ) . toBe ( 'ENG-103' )
509+ } )
510+
511+ it . concurrent ( 'keeps the summary and status of a Jira issue macro Confluence resolved' , ( ) => {
512+ const html =
513+ '<span class="confluence-jim-macro jira-issue resolved" data-jira-key="OPS-201">' +
514+ '<a href="https://example.atlassian.net/browse/OPS-201" class="jira-issue-key">' +
515+ '<img class="icon" src="https://example.atlassian.net/avatar.png" />OPS-201</a> - ' +
516+ '<span class="summary">Rotate the signing key</span> ' +
517+ '<span class="aui-lozenge aui-lozenge-success jira-macro-single-issue-export-pdf">Done</span>' +
518+ '</span>'
519+ expect ( confluenceViewToPlainText ( html ) ) . toBe ( 'OPS-201 - Rotate the signing key Done' )
520+ } )
521+
522+ it . concurrent ( 'keeps the block break of a Jira issue macro inside a callout' , ( ) => {
523+ const html =
524+ '<div class="confluence-information-macro confluence-information-macro-warning">' +
525+ '<div class="confluence-information-macro-body">Blocked by' +
526+ '<div class="confluence-jim-macro jira-issue" data-jira-key="ENG-104">' +
527+ '<a class="jira-issue-key"><span class="aui-icon issue-placeholder"></span>ENG-104</a> - ' +
528+ '<span class="summary">Getting issue details...</span>' +
529+ '<span class="aui-lozenge issue-placeholder">STATUS</span></div>' +
530+ 'until release</div></div>'
531+ expect ( confluenceViewToPlainText ( html ) ) . toBe ( '[WARNING] Blocked by ENG-104 until release' )
532+ } )
533+
534+ it . concurrent ( 'drops only the placeholder shell of a Jira issues table' , ( ) => {
535+ const html =
536+ '<p>Release notes</p>' +
537+ '<div class="confluence-jim-macro refresh-module-id jira-table placeholder conf-macro output-block">' +
538+ '<div class="jira-issues"><table class="aui"><tbody><tr></tr>' +
539+ '<tr><th>type</th><th>key</th><th>summary</th></tr></tbody></table></div>' +
540+ '<div class="refresh-issues-bottom"><span class="aui-icon aui-icon-wait">Loading...</span></div></div>' +
541+ '<div class="confluence-jim-macro jira-table"><table class="aui"><tbody>' +
542+ '<tr><th>key</th><th>summary</th></tr><tr><td>ENG-104</td><td>Update the runbook</td></tr>' +
543+ '</tbody></table></div>' +
544+ '<div class="confluence-jim-macro jira-table"><div class="aui-message">No issues found</div></div>'
545+ expect ( confluenceViewToPlainText ( html ) ) . toBe (
546+ 'Release notes key summary ENG-104 Update the runbook No issues found'
547+ )
548+ } )
462549} )
463550
464551describe ( 'confluence incremental CQL listing' , ( ) => {
@@ -1055,7 +1142,29 @@ describe('Confluence permission-scoped content', () => {
10551142 )
10561143
10571144 expect ( document ?. content ) . toContain ( 'CONFIDENTIAL SALARY DATA' )
1058- expect ( document ?. contentHash ) . toBe ( 'confluence:view-callouts:shared-page:1' )
1145+ expect ( document ?. contentHash ) . toBe ( 'confluence:view-text-v2:shared-page:1' )
1146+ } )
1147+
1148+ it ( 'reports the content type of the endpoint that answered and omits unknown metadata' , async ( ) => {
1149+ vi . mocked ( fetch ) . mockResolvedValueOnce ( new Response ( '' , { status : 404 } ) )
1150+
1151+ const document = await confluenceConnector . getDocument ( 'token' , config , 'shared-page' , {
1152+ cloudId : 'cloud-1' ,
1153+ } )
1154+
1155+ expect ( vi . mocked ( fetch ) . mock . calls . map ( ( [ input ] ) => new URL ( String ( input ) ) . pathname ) ) . toEqual ( [
1156+ '/ex/confluence/cloud-1/wiki/api/v2/pages/shared-page' ,
1157+ '/ex/confluence/cloud-1/wiki/api/v2/blogposts/shared-page' ,
1158+ ] )
1159+ expect ( document ?. metadata ) . toEqual ( {
1160+ spaceId : 'space-1' ,
1161+ contentType : 'blogpost' ,
1162+ status : 'current' ,
1163+ version : 1 ,
1164+ labels : [ ] ,
1165+ lastModified : '' ,
1166+ } )
1167+ expect ( document ?. metadata ) . not . toHaveProperty ( 'spaceKey' )
10591168 } )
10601169
10611170 it ( 'rejects a missing storage body without falling back to rendered content' , async ( ) => {
@@ -1118,7 +1227,7 @@ describe('Confluence permission-scoped content', () => {
11181227 const expectedHash =
11191228 'mirrorsSourceAcls' in mode || 'perMemberListing' in mode
11201229 ? 'confluence:storage-local-body-v2:shared-page:1'
1121- : 'confluence:view-callouts :shared-page:1'
1230+ : 'confluence:view-text-v2 :shared-page:1'
11221231
11231232 expect ( v2 . documents [ 0 ] . contentHash ) . toBe ( expectedHash )
11241233 expect ( cql . documents [ 0 ] . contentHash ) . toBe ( expectedHash )
0 commit comments