Skip to content

Commit 4ef52fd

Browse files
authored
fix(confluence): drop scripts, styles, and unresolved Jira placeholders from indexed page text (#7831)
1 parent 8a4a0e1 commit 4ef52fd

3 files changed

Lines changed: 194 additions & 43 deletions

File tree

apps/sim/connectors/confluence/confluence.test.ts

Lines changed: 137 additions & 28 deletions
Original file line numberDiff line numberDiff line change
@@ -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'
2020
import { extractCursor } from '@/connectors/confluence/cursor'
21-
import { htmlToPlainText } from '@/connectors/utils'
2221

2322
describe('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(/Nested item 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

464551
describe('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

Comments
 (0)