Skip to content

Commit 06c3b09

Browse files
committed
Prevent undesired duplicate elements
Check if each new element matches an existing element before adding it to the output DOM. For example: ```css button { color: red; } /* ... */ button { color: blue; } ``` Will now produce one blue button instead of two separate buttons.
1 parent 20c5850 commit 06c3b09

File tree

2 files changed

+45
-5
lines changed

2 files changed

+45
-5
lines changed

src/Generator.ts

Lines changed: 34 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,5 @@
1+
type Combinator = '>' | '~' | '+';
2+
13
/**
24
* Generate an HTML document from CSS.
35
* @param css The style sheet.
@@ -45,8 +47,8 @@ export function cssToHtml(css: CSSRuleList | string): HTMLBodyElement {
4547
const descriptor = {
4648
previousElement: undefined as HTMLElement | undefined,
4749
previousCharacter: '',
48-
combinator: '',
49-
addressCharacter: '',
50+
combinator: '' as '' | Combinator,
51+
addressCharacter: '' as '' | '.' | '#',
5052
classes: [''],
5153
id: '',
5254
tag: '',
@@ -68,11 +70,38 @@ export function cssToHtml(css: CSSRuleList | string): HTMLBodyElement {
6870
descriptor.classes = [''];
6971
descriptor.id = '';
7072
descriptor.tag = '';
73+
},
74+
matchesElement: (element: Element): boolean => {
75+
const descriptorTag = (descriptor.tag.toUpperCase() || 'DIV');
76+
const descriptorClasses = descriptor.classes.filter((c) => Boolean(c));
77+
// Compare tag, id, and classlist length.
78+
if (
79+
element.tagName !== descriptorTag
80+
|| element.id !== descriptor.id
81+
|| element.classList.length !== descriptorClasses.length
82+
) {
83+
return false;
84+
}
85+
// Compare classlists.
86+
const differingClasses = descriptorClasses.filter((c) => !element.classList.contains(c));
87+
return !differingClasses.length;
7188
}
7289
};
7390

7491
function addElementToOutput (): void {
75-
// Create the element.
92+
// If an identical element already exists, skip adding the new element.
93+
const existingElements = Array.from(
94+
(descriptor.combinator === '>' ?
95+
descriptor.previousElement?.children :
96+
descriptor.previousElement?.parentElement?.children) ?? output.children
97+
);
98+
const matchingElement = existingElements.find((element) => descriptor.matchesElement(element));
99+
if (matchingElement) {
100+
// Reference the matching element so properties such as `content` can cascade.
101+
descriptor.previousElement = matchingElement as HTMLElement;
102+
return;
103+
}
104+
// Create the new element.
76105
const newElement = document.createElement(descriptor.tag || 'div');
77106
// Add the classes.
78107
for (const c of descriptor.classes) {
@@ -105,7 +134,7 @@ export function cssToHtml(css: CSSRuleList | string): HTMLBodyElement {
105134
// The start of a new selector.
106135
if (!descriptor.previousCharacter) {
107136
if (/(?:\+|~|>)/.test(character)) {
108-
descriptor.combinator = character;
137+
descriptor.combinator = character as Combinator;
109138
}
110139
else if (character === '.' || character === '#') {
111140
descriptor.addressCharacter = character;
@@ -130,7 +159,7 @@ export function cssToHtml(css: CSSRuleList | string): HTMLBodyElement {
130159
else if (/(?:\+|~|>)/.test(character)) {
131160
addElementToOutput();
132161
descriptor.clear();
133-
descriptor.combinator = character;
162+
descriptor.combinator = character as Combinator;
134163
}
135164
// The character none of the above.
136165
else {

tests/index.js

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,10 @@ a {
1111
content: 'https://example.com/';
1212
border-radius: 10px;
1313
}
14+
a {
15+
content: 'https://example.com/page';
16+
border-radius: 10px;
17+
}
1418
button {
1519
content: 'Click me';
1620
background-color: red;
@@ -27,9 +31,16 @@ nav a#logo.icon> img {
2731
content: 'https://example.com/image';
2832
display: block;
2933
}
34+
nav a#logo.icon > img {
35+
content: 'https://example.com/image2';
36+
}
3037
.pie .pastry.crenelations {
3138
background: radial-gradient(circle at center, orange 10%, yellow);
3239
}
40+
button {
41+
content: 'Double-click me';
42+
background-color: blue;
43+
}
3344
`;
3445

3546
const output = cssToHtml(input);

0 commit comments

Comments
 (0)