Skip to content

Commit 26df44f

Browse files
committed
feat(ui): skip decorative items in sidebar
1 parent 0c69fa1 commit 26df44f

File tree

2 files changed

+327
-1
lines changed

2 files changed

+327
-1
lines changed
Lines changed: 315 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,315 @@
1+
import {afterEach, beforeEach, describe, expect, it} from 'vitest';
2+
import {createSidebar} from '../sidebar';
3+
4+
describe('createSidebar', () => {
5+
beforeEach(() => {
6+
document.body.innerHTML = '';
7+
});
8+
9+
afterEach(() => {
10+
document.body.innerHTML = '';
11+
});
12+
13+
describe('sidebar container', () => {
14+
it('creates a sidebar element with correct class and style', () => {
15+
const sidebar = createSidebar();
16+
17+
expect(sidebar.className).toBe('sidebar');
18+
expect(sidebar.style.position).toBe('fixed');
19+
});
20+
21+
it('appends the sidebar to the document body', () => {
22+
createSidebar();
23+
24+
const sidebar = document.querySelector('.sidebar');
25+
expect(sidebar).not.toBeNull();
26+
expect(sidebar?.parentElement).toBe(document.body);
27+
});
28+
29+
it('contains an ordered list element', () => {
30+
const sidebar = createSidebar();
31+
32+
const list = sidebar.querySelector('ol');
33+
expect(list).not.toBeNull();
34+
});
35+
});
36+
37+
describe('navigation items generation', () => {
38+
it('creates navigation items for h1 elements', () => {
39+
document.body.innerHTML = '<h1 id="title">Main Title</h1>';
40+
const sidebar = createSidebar();
41+
42+
const items = sidebar.querySelectorAll('li');
43+
expect(items.length).toBe(1);
44+
expect(items[0].classList.contains('h1')).toBe(true);
45+
expect(items[0].querySelector('a')?.getAttribute('href')).toBe('#title');
46+
expect(items[0].querySelector('span')?.textContent).toBe('Main Title');
47+
});
48+
49+
it('creates navigation items for h2 elements', () => {
50+
document.body.innerHTML = '<h2 id="section">Section Header</h2>';
51+
const sidebar = createSidebar();
52+
53+
const items = sidebar.querySelectorAll('li');
54+
expect(items.length).toBe(1);
55+
expect(items[0].classList.contains('h2')).toBe(true);
56+
expect(items[0].querySelector('a')?.getAttribute('href')).toBe('#section');
57+
expect(items[0].querySelector('span')?.textContent).toBe('Section Header');
58+
});
59+
60+
it('creates navigation items for h3 elements', () => {
61+
document.body.innerHTML = '<h3 id="subsection">Subsection</h3>';
62+
const sidebar = createSidebar();
63+
64+
const items = sidebar.querySelectorAll('li');
65+
expect(items.length).toBe(1);
66+
expect(items[0].classList.contains('h3')).toBe(true);
67+
expect(items[0].querySelector('a')?.getAttribute('href')).toBe('#subsection');
68+
expect(items[0].querySelector('span')?.textContent).toBe('Subsection');
69+
});
70+
71+
it('ignores h4, h5, h6 elements', () => {
72+
document.body.innerHTML = `
73+
<h4 id="h4">H4 Title</h4>
74+
<h5 id="h5">H5 Title</h5>
75+
<h6 id="h6">H6 Title</h6>
76+
`;
77+
const sidebar = createSidebar();
78+
79+
const items = sidebar.querySelectorAll('li');
80+
expect(items.length).toBe(0);
81+
});
82+
83+
it('creates items for mixed heading levels in document order', () => {
84+
document.body.innerHTML = `
85+
<h1 id="title">Title</h1>
86+
<h2 id="section1">Section 1</h2>
87+
<h3 id="subsection1">Subsection 1.1</h3>
88+
<h2 id="section2">Section 2</h2>
89+
<h3 id="subsection2">Subsection 2.1</h3>
90+
`;
91+
const sidebar = createSidebar();
92+
93+
const items = sidebar.querySelectorAll('li');
94+
expect(items.length).toBe(5);
95+
96+
expect(items[0].classList.contains('h1')).toBe(true);
97+
expect(items[0].querySelector('span')?.textContent).toBe('Title');
98+
99+
expect(items[1].classList.contains('h2')).toBe(true);
100+
expect(items[1].querySelector('span')?.textContent).toBe('Section 1');
101+
102+
expect(items[2].classList.contains('h3')).toBe(true);
103+
expect(items[2].querySelector('span')?.textContent).toBe('Subsection 1.1');
104+
105+
expect(items[3].classList.contains('h2')).toBe(true);
106+
expect(items[3].querySelector('span')?.textContent).toBe('Section 2');
107+
108+
expect(items[4].classList.contains('h3')).toBe(true);
109+
expect(items[4].querySelector('span')?.textContent).toBe('Subsection 2.1');
110+
});
111+
});
112+
113+
describe('data-decorative heading filtering', () => {
114+
it('excludes h1 with data-decorative attribute', () => {
115+
document.body.innerHTML = `
116+
<h1 id="regular">Regular Title</h1>
117+
<h1 id="decorative" data-decorative>Decorative Title</h1>
118+
`;
119+
const sidebar = createSidebar();
120+
121+
const items = sidebar.querySelectorAll('li');
122+
expect(items.length).toBe(1);
123+
expect(items[0].querySelector('span')?.textContent).toBe('Regular Title');
124+
});
125+
126+
it('excludes h2 with data-decorative attribute', () => {
127+
document.body.innerHTML = `
128+
<h2 id="regular">Regular Section</h2>
129+
<h2 id="decorative" data-decorative>Decorative Section</h2>
130+
`;
131+
const sidebar = createSidebar();
132+
133+
const items = sidebar.querySelectorAll('li');
134+
expect(items.length).toBe(1);
135+
expect(items[0].querySelector('span')?.textContent).toBe('Regular Section');
136+
});
137+
138+
it('excludes h3 with data-decorative attribute', () => {
139+
document.body.innerHTML = `
140+
<h3 id="regular">Regular Subsection</h3>
141+
<h3 id="decorative" data-decorative>Decorative Subsection</h3>
142+
`;
143+
const sidebar = createSidebar();
144+
145+
const items = sidebar.querySelectorAll('li');
146+
expect(items.length).toBe(1);
147+
expect(items[0].querySelector('span')?.textContent).toBe('Regular Subsection');
148+
});
149+
150+
it('excludes all decorative headings in mixed environments', () => {
151+
document.body.innerHTML = `
152+
<h1 id="title" data-decorative>Decorative Title</h1>
153+
<h2 id="section1">Section 1</h2>
154+
<h3 id="subsection1" data-decorative>Decorative Subsection</h3>
155+
<h2 id="section2" data-decorative>Decorative Section</h2>
156+
<h3 id="subsection2">Subsection 2</h3>
157+
`;
158+
const sidebar = createSidebar();
159+
160+
const items = sidebar.querySelectorAll('li');
161+
expect(items.length).toBe(2);
162+
expect(items[0].querySelector('span')?.textContent).toBe('Section 1');
163+
expect(items[1].querySelector('span')?.textContent).toBe('Subsection 2');
164+
});
165+
166+
it('handles data-decorative with empty string value', () => {
167+
document.body.innerHTML = `
168+
<h1 id="title" data-decorative="">Decorative Title</h1>
169+
<h2 id="section">Regular Section</h2>
170+
`;
171+
const sidebar = createSidebar();
172+
173+
const items = sidebar.querySelectorAll('li');
174+
expect(items.length).toBe(1);
175+
expect(items[0].querySelector('span')?.textContent).toBe('Regular Section');
176+
});
177+
178+
it('handles data-decorative with truthy value', () => {
179+
document.body.innerHTML = `
180+
<h1 id="title" data-decorative="true">Decorative Title</h1>
181+
<h2 id="section">Regular Section</h2>
182+
`;
183+
const sidebar = createSidebar();
184+
185+
const items = sidebar.querySelectorAll('li');
186+
expect(items.length).toBe(1);
187+
expect(items[0].querySelector('span')?.textContent).toBe('Regular Section');
188+
});
189+
});
190+
191+
describe('edge cases', () => {
192+
it('creates empty sidebar when no headings exist', () => {
193+
document.body.innerHTML = '<p>Some content without headings</p>';
194+
const sidebar = createSidebar();
195+
196+
const items = sidebar.querySelectorAll('li');
197+
expect(items.length).toBe(0);
198+
});
199+
200+
it('creates empty sidebar when all headings are decorative', () => {
201+
document.body.innerHTML = `
202+
<h1 id="title" data-decorative>Decorative Title</h1>
203+
<h2 id="section" data-decorative>Decorative Section</h2>
204+
`;
205+
const sidebar = createSidebar();
206+
207+
const items = sidebar.querySelectorAll('li');
208+
expect(items.length).toBe(0);
209+
});
210+
211+
it('handles headings with empty text content', () => {
212+
document.body.innerHTML = '<h1 id="empty"></h1>';
213+
const sidebar = createSidebar();
214+
215+
const items = sidebar.querySelectorAll('li');
216+
expect(items.length).toBe(1);
217+
expect(items[0].querySelector('span')?.textContent).toBe('');
218+
});
219+
220+
it('handles headings with nested HTML content', () => {
221+
document.body.innerHTML = '<h1 id="nested"><strong>Bold</strong> and <em>italic</em></h1>';
222+
const sidebar = createSidebar();
223+
224+
const items = sidebar.querySelectorAll('li');
225+
expect(items.length).toBe(1);
226+
expect(items[0].querySelector('span')?.textContent).toBe('Bold and italic');
227+
});
228+
229+
it('handles headings with special characters', () => {
230+
document.body.innerHTML = '<h1 id="special">Title with &amp; "quotes" &apos;apostrophes&apos;</h1>';
231+
const sidebar = createSidebar();
232+
233+
const items = sidebar.querySelectorAll('li');
234+
expect(items.length).toBe(1);
235+
expect(items[0].querySelector('span')?.textContent).toBe('Title with & "quotes" \'apostrophes\'');
236+
});
237+
238+
it('handles headings without id attribute', () => {
239+
document.body.innerHTML = '<h1>No ID Title</h1>';
240+
const sidebar = createSidebar();
241+
242+
const items = sidebar.querySelectorAll('li');
243+
expect(items.length).toBe(1);
244+
expect(items[0].querySelector('a')?.getAttribute('href')).toBe('#');
245+
});
246+
247+
it('handles multiple calls creating multiple sidebars', () => {
248+
document.body.innerHTML = '<h1 id="title">Title</h1>';
249+
250+
createSidebar();
251+
createSidebar();
252+
253+
const sidebars = document.querySelectorAll('.sidebar');
254+
expect(sidebars.length).toBe(2);
255+
});
256+
});
257+
258+
describe('complex document structures', () => {
259+
it('processes headings inside nested containers', () => {
260+
document.body.innerHTML = `
261+
<div class="container">
262+
<article>
263+
<h1 id="title">Title</h1>
264+
<section>
265+
<h2 id="section">Section</h2>
266+
</section>
267+
</article>
268+
</div>
269+
`;
270+
const sidebar = createSidebar();
271+
272+
const items = sidebar.querySelectorAll('li');
273+
expect(items.length).toBe(2);
274+
expect(items[0].querySelector('span')?.textContent).toBe('Title');
275+
expect(items[1].querySelector('span')?.textContent).toBe('Section');
276+
});
277+
278+
it('handles headings scattered across multiple containers', () => {
279+
document.body.innerHTML = `
280+
<header>
281+
<h1 id="main-title">Main Title</h1>
282+
</header>
283+
<main>
284+
<h2 id="section1">Section 1</h2>
285+
</main>
286+
<footer>
287+
<h3 id="footer-heading">Footer Heading</h3>
288+
</footer>
289+
`;
290+
const sidebar = createSidebar();
291+
292+
const items = sidebar.querySelectorAll('li');
293+
expect(items.length).toBe(3);
294+
});
295+
296+
it('maintains document order regardless of DOM depth', () => {
297+
document.body.innerHTML = `
298+
<h1 id="first">First</h1>
299+
<div>
300+
<div>
301+
<h2 id="second">Second</h2>
302+
</div>
303+
</div>
304+
<h3 id="third">Third</h3>
305+
`;
306+
const sidebar = createSidebar();
307+
308+
const items = sidebar.querySelectorAll('li');
309+
expect(items.length).toBe(3);
310+
expect(items[0].querySelector('span')?.textContent).toBe('First');
311+
expect(items[1].querySelector('span')?.textContent).toBe('Second');
312+
expect(items[2].querySelector('span')?.textContent).toBe('Third');
313+
});
314+
});
315+
});

quarkdown-html/src/main/typescript/sidebar/sidebar.ts

Lines changed: 12 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -58,6 +58,17 @@ function createActiveStateChecker(
5858
};
5959
}
6060

61+
/**
62+
* Retrieves all h1, h2, and h3 header elements that are not marked as decorative.
63+
*
64+
* @returns An array of header elements
65+
*/
66+
function getHeadings(): HTMLElement[] {
67+
const selection = document.querySelectorAll<HTMLElement>('h1, h2, h3');
68+
return Array.from(selection)
69+
.filter(header => !header.hasAttribute('data-decorative'));
70+
}
71+
6172
/**
6273
* Processes all header elements and creates navigation items with active state tracking.
6374
*
@@ -72,7 +83,7 @@ function populateNavigationItems(sidebarList: HTMLOListElement): void {
7283
currentActiveListItem = item;
7384
};
7485

75-
document.querySelectorAll('h1, h2, h3').forEach(header => {
86+
getHeadings().forEach(header => {
7687
const listItem = createNavigationItem(header);
7788
sidebarList.appendChild(listItem);
7889

0 commit comments

Comments
 (0)