import { load } from 'cheerio'; import { applyConfluenceMarginLeftIndent } from './confluence-indent'; function run(html: string): string { const $ = load(html); applyConfluenceMarginLeftIndent($, $.root()); // cheerio's html() includes ; return the body's inner HTML so // tests can assert on the meaningful portion. return $('body').html() ?? $.html(); } describe('applyConfluenceMarginLeftIndent', () => { describe('Confluence Cloud (30 px per level, max 6)', () => { it('maps 30/60/90/120/150/180 px to data-indent 1..6', () => { const html = '

L1

' + '

L2

' + '

L3

' + '

L4

' + '

L5

' + '

L6

'; const out = run(html); expect(out).toContain('

L1

'); expect(out).toContain('

L2

'); expect(out).toContain('

L3

'); expect(out).toContain('

L4

'); expect(out).toContain('

L5

'); expect(out).toContain('

L6

'); expect(out).not.toContain('margin-left'); }); }); describe('Confluence Data Center (40 px per level, no upper bound)', () => { it('maps 40/80/120/160/200/240 px to data-indent 1..6', () => { const html = '

one

' + '

two

' + '

three

' + '

four

' + '

five

' + '

six

'; const out = run(html); expect(out).toContain('

one

'); expect(out).toContain('

two

'); expect(out).toContain('

three

'); expect(out).toContain('

four

'); expect(out).toContain('

five

'); expect(out).toContain('

six

'); expect(out).not.toContain('margin-left'); }); it('clamps DC levels above 8 down to 8', () => { const html = '

L8

' + '

L9

' + '

L15

'; const out = run(html); expect(out).toContain('

L8

'); expect(out).toContain('

L9

'); expect(out).toContain('

L15

'); }); }); describe('headings', () => { it('handles indent on h1-h6 the same way as paragraphs', () => { const html = '

a

' + '
b
'; const out = run(html); expect(out).toContain('

a

'); expect(out).toContain('
b
'); }); }); describe('style attribute handling', () => { it('strips margin-left but preserves other inline styles', () => { const html = '

x

'; const out = run(html); expect(out).toMatch(/

x<\/p>/); expect(out).not.toContain('margin-left'); }); it('removes the style attribute entirely when only margin-left was set', () => { // Two values so GCD detection sees a real unit (60 px) instead of // collapsing to the lone value. The point of this test is the style // attribute being stripped, not the level number. const html = '

x

' + '

y

'; const out = run(html); expect(out).toContain('

x

'); expect(out).toContain('

y

'); expect(out).not.toContain('style='); }); }); describe('scope and edge cases', () => { it('leaves elements without margin-left untouched', () => { const html = '

plain

heading

'; const out = run(html); expect(out).toBe('

plain

heading

'); }); it('does not touch divs, spans, or list items', () => { const html = '
div
' + '
  • li
  • ' + 'span'; const out = run(html); expect(out).not.toContain('data-indent'); expect(out).toContain('margin-left: 30px'); }); it('ignores zero, negative, and unparseable margin-left values', () => { const html = '

    zero

    ' + '

    neg

    ' + '

    auto

    '; const out = run(html); expect(out).not.toContain('data-indent'); }); it('honors an explicit pxPerLevel override', () => { // Mixed Cloud-and-DC nominal values forced to 40 px/level interpretation. const $ = load( '

    a

    ' + '

    b

    ', ); applyConfluenceMarginLeftIndent($, $.root(), { pxPerLevel: 40 }); const out = $('body').html() ?? ''; expect(out).toContain('

    a

    '); expect(out).toContain('

    b

    '); }); it('returns a no-op when no indented elements are present', () => { const html = '

    hi

    '; const out = run(html); expect(out).toBe('

    hi

    '); }); it('handles a single ambiguous value by clamping to level 1', () => { // GCD of a single value is the value itself, so 120 / 120 = 1. const html = '

    only

    '; const out = run(html); expect(out).toContain('

    only

    '); }); }); });