Skip to content

Commit fed2005

Browse files
committed
Improve generation of nth elements
1 parent 983fa5b commit fed2005

File tree

2 files changed

+75
-4
lines changed

2 files changed

+75
-4
lines changed

src/Generator.ts

Lines changed: 37 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,8 @@ type Combinator = '>' | '~' | '+';
22

33
interface Options {
44
duplicates?: 'preserve' | 'remove';
5-
fill?: 'fill' | 'no-fill'
5+
fill?: 'fill' | 'no-fill';
6+
mergeNth?: 'merge' | 'no-merge'
67
}
78

89
/**
@@ -21,6 +22,20 @@ export function cssToHtml(css: CSSRuleList | string, options: Options = {}): HTM
2122
}
2223
return false;
2324
}
25+
function mergeElements <T extends HTMLElement | Element> (mergeFrom: HTMLElement | Element, mergeTo: T): T | null {
26+
if (mergeFrom.tagName !== mergeTo.tagName) {
27+
return null;
28+
}
29+
if (mergeFrom.id && mergeTo.id && mergeFrom.id !== mergeTo.id) {
30+
return null;
31+
}
32+
if (mergeFrom.id) {
33+
mergeTo.id = mergeFrom.id;
34+
}
35+
mergeTo.className += ' ' + mergeFrom.className;
36+
mergeTo.className = mergeTo.className.trim();
37+
return mergeTo;
38+
}
2439
let styleRules: CSSRuleList | undefined;
2540

2641
// Parse the CSS string into a CSSOM.
@@ -202,20 +217,38 @@ export function cssToHtml(css: CSSRuleList | string, options: Options = {}): HTM
202217
descriptor.previousElement = newElement;
203218

204219
if (fillType === 'first') {
205-
parentElement.prepend(newElement);
220+
// Check if there is a sibling element in the desired position.
221+
const blockingSibling = parentElement.querySelector(childType === 'type' ? `${newElement.tagName}:first-of-type` : '*:first-child');
222+
if (blockingSibling) {
223+
parentElement.insertBefore(newElement, blockingSibling);
224+
if (isFillerElement(blockingSibling) || (options.mergeNth !== 'no-merge' && mergeElements(blockingSibling, newElement))) {
225+
blockingSibling.remove();
226+
}
227+
} else {
228+
parentElement.prepend(newElement);
229+
}
206230
return;
207231
}
208232

209233
if (fillType === 'last') {
210-
parentElement.append(newElement);
234+
// Check if there is a sibling element in the desired position.
235+
const blockingSibling = parentElement.querySelector(childType === 'type' ? `${newElement.tagName}:last-of-type` : `${newElement.tagName}:last-child`);
236+
if (blockingSibling) {
237+
parentElement.insertBefore(newElement, blockingSibling.nextElementSibling);
238+
if (isFillerElement(blockingSibling) || (options.mergeNth !== 'no-merge' && mergeElements(blockingSibling, newElement))) {
239+
blockingSibling.remove();
240+
}
241+
} else {
242+
parentElement.append(newElement);
243+
}
211244
return;
212245
}
213246

214247
// Check if there is a sibling element in the desired position.
215248
const blockingSibling = parentElement.querySelector(childType === 'type' ? `${newElement.tagName}:nth-of-type(${fillAmount})` : `:nth-child(${fillAmount})`);
216249
if (blockingSibling) {
217250
parentElement.insertBefore(newElement, blockingSibling);
218-
if (isFillerElement(blockingSibling)) {
251+
if (isFillerElement(blockingSibling) || (options.mergeNth !== 'no-merge' && mergeElements(blockingSibling, newElement))) {
219252
blockingSibling.remove();
220253
}
221254
return;

tests/nth-child.spec.ts

Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,38 @@
1+
/*
2+
* Verify that nth-child selectors will be handled correctly.
3+
*/
4+
5+
import { test, expect } from '@playwright/test';
6+
import { cssToHtml } from '../src/index';
7+
8+
const css = `
9+
span:nth-child(10) {
10+
color: red;
11+
}
12+
div.first:first-child {
13+
content: 'a';
14+
}
15+
span.last:last-child {
16+
content: 'b';
17+
}
18+
span:last-child {
19+
content: 'c';
20+
}
21+
`;
22+
23+
test('Nth-Child', async ({ page }) => {
24+
await page.addScriptTag({ path: './tests/GeneratorScript.js' });
25+
26+
const result = await page.evaluate(async (css) => {
27+
document.body = cssToHtml(css);
28+
29+
return document.body.querySelectorAll('*').length === 10
30+
&& document.body.querySelector('div.first')?.previousElementSibling === null
31+
&& document.body.querySelector('div.first')?.innerHTML === 'a'
32+
&& document.body.querySelector('span.last')?.nextElementSibling === null
33+
&& document.body.querySelector('span.last')?.innerHTML === 'c';
34+
}, css);
35+
36+
expect(result).toBeDefined();
37+
expect(result).toBe(true);
38+
});

0 commit comments

Comments
 (0)