Skip to content

Commit f379ebc

Browse files
committed
fix(confluence): preserve word boundaries when extracting callout body text
Greptile P1: cheerio's .text() concatenates every descendant text node with no separator, so pulling a macro body's text in one call fused adjacent blocks together (e.g. a paragraph ending in "for:" immediately followed by a list item "GitLab" became "for:GitLab"), corrupting the exact word boundaries RAG chunking and keyword matching depend on. extractBlockJoinedText now extracts each paragraph/list-item/heading/cell/quote individually and joins them with a single space, keeping every block's text intact and properly separated, matching how htmlToPlainText already treats the rest of the page.
1 parent 89c58cc commit f379ebc

2 files changed

Lines changed: 59 additions & 4 deletions

File tree

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

Lines changed: 30 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -129,9 +129,38 @@ describe('preserveConfluenceCallouts', () => {
129129
'</div>\n\n' +
130130
'<p>Trailing paragraph.</p>'
131131
const plainText = htmlToPlainText(preserveConfluenceCallouts(html))
132-
expect(plainText).toContain('[WARNING] Do NOT use this form for:GitLab')
132+
expect(plainText).toContain('[WARNING] Do NOT use this form for: GitLab')
133133
expect(plainText).toContain('Intro paragraph.')
134134
expect(plainText).toContain('Trailing paragraph.')
135135
}
136136
)
137+
138+
it.concurrent(
139+
'does not fuse adjacent paragraph and list-item text together (word-boundary regression)',
140+
() => {
141+
const html =
142+
'<div class="confluence-information-macro confluence-information-macro-warning">' +
143+
'<div class="confluence-information-macro-body">' +
144+
'<p>Do NOT use this form for:</p>' +
145+
'<ul><li>GitLab</li><li>ServiceNow</li></ul>' +
146+
'</div></div>'
147+
const result = preserveConfluenceCallouts(html)
148+
expect(result).not.toContain('for:GitLab')
149+
expect(result).not.toContain('GitLabServiceNow')
150+
expect(result).toContain('Do NOT use this form for: GitLab ServiceNow')
151+
}
152+
)
153+
154+
it.concurrent(
155+
'preserves word boundaries across multiple paragraphs in a generic Panel macro',
156+
() => {
157+
const html =
158+
'<div class="panel"><div class="panelContent">' +
159+
'<p>First sentence.</p><p>Second sentence.</p>' +
160+
'</div></div>'
161+
const result = preserveConfluenceCallouts(html)
162+
expect(result).toContain('First sentence. Second sentence.')
163+
expect(result).not.toContain('sentence.Second')
164+
}
165+
)
137166
})

apps/sim/connectors/confluence/confluence.ts

Lines changed: 29 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,31 @@ const CALLOUT_LABELS: Record<string, string> = {
1818
error: '[ERROR]',
1919
}
2020

21+
/**
22+
* Selectors for elements whose own text is a natural word-boundary unit —
23+
* paragraphs, list items, headings, table cells, quotes. Cheerio's `.text()`
24+
* concatenates every descendant text node with no separator at all, so pulling
25+
* a macro body's text in one call fuses adjacent blocks together (e.g. a
26+
* `<p>...for:</p>` immediately followed by `<li>GitLab</li>` becomes
27+
* `for:GitLab`, corrupting the very word boundaries RAG chunking depends on).
28+
* Extracting block-by-block and joining with a single space keeps each block's
29+
* text intact and separated, matching how `htmlToPlainText` already treats the
30+
* rest of the page.
31+
*/
32+
const BLOCK_TEXT_SELECTOR = 'p, li, h1, h2, h3, h4, h5, h6, td, th, blockquote, pre'
33+
34+
function extractBlockJoinedText($: cheerio.CheerioAPI, $el: cheerio.Cheerio<any>): string {
35+
const blocks = $el.find(BLOCK_TEXT_SELECTOR)
36+
if (blocks.length === 0) {
37+
return $el.text().trim()
38+
}
39+
return blocks
40+
.map((_, block) => $(block).text().trim())
41+
.get()
42+
.filter(Boolean)
43+
.join(' ')
44+
}
45+
2146
/**
2247
* Confluence's rendered `view` HTML wraps Info/Note/Warning/Tip macros in
2348
* `confluence-information-macro confluence-information-macro-{type}` divs, and
@@ -40,15 +65,16 @@ export function preserveConfluenceCallouts(html: string): string {
4065
.match(/confluence-information-macro-(\w+)/)?.[1]
4166
?.toLowerCase()
4267
const label = (type && CALLOUT_LABELS[type]) || CALLOUT_LABELS.information
43-
const body =
44-
$el.find('.confluence-information-macro-body').first().text().trim() || $el.text().trim()
68+
const macroBody = $el.find('.confluence-information-macro-body').first()
69+
const body = extractBlockJoinedText($, macroBody.length > 0 ? macroBody : $el)
4570
$el.replaceWith($('<p></p>').text(`${label} ${body}`))
4671
})
4772

4873
$('div.panel').each((_, el) => {
4974
const $el = $(el)
5075
const headerText = $el.find('.panelHeader').first().text().trim()
51-
const bodyText = $el.find('.panelContent').first().text().trim() || $el.text().trim()
76+
const panelContent = $el.find('.panelContent').first()
77+
const bodyText = extractBlockJoinedText($, panelContent.length > 0 ? panelContent : $el)
5278
const label = headerText ? `[CALLOUT: ${headerText}]` : '[CALLOUT]'
5379
$el.replaceWith($('<p></p>').text(`${label} ${bodyText}`))
5480
})

0 commit comments

Comments
 (0)