Skip to content

Commit 2569987

Browse files
committed
Rewrite the HTML generator
- Use the `css-selector-parser` library to parse CSS selectors. - Use a slightly more OOP approach. - Add `imports` option. - Export an Options class from the package.
1 parent 2a343fd commit 2569987

File tree

6 files changed

+443
-370
lines changed

6 files changed

+443
-370
lines changed

package-lock.json

Lines changed: 23 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

package.json

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -32,5 +32,8 @@
3232
"homepage": "https://github.com/CSS-Canvas/CSS-to-HTML#readme",
3333
"devDependencies": {
3434
"@playwright/test": "^1.35.1"
35+
},
36+
"dependencies": {
37+
"css-selector-parser": "^2.3.2"
3538
}
3639
}

src/Descriptor.ts

Lines changed: 184 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,184 @@
1+
import type { AstRule } from 'css-selector-parser';
2+
import { replaceTextNode } from './Utility.js';
3+
4+
/**
5+
* Valid positioning pseudo classes.
6+
*/
7+
const positioningPseudoClasses = [
8+
'first-child',
9+
'nth-child',
10+
'nth-last-child',
11+
'last-child',
12+
'first-of-type',
13+
'nth-of-type',
14+
'nth-last-of-type',
15+
'last-of-type'
16+
];
17+
18+
/**
19+
* Parse the position formula of `nth` pseudo classes.
20+
*
21+
* Position formulae are expressed as `an + b`, where `a` and `b` are either `0` or positive integers.
22+
* @param a
23+
* @param b
24+
* @returns A positive integer representing the desired position of the selector, or `false` if the position is invalid.
25+
*/
26+
function parsePositionFormula (a: number, b: number): number | false {
27+
// Invalid.
28+
if (a < 0 || b < 0) {
29+
return false;
30+
}
31+
// Invalid.
32+
if (!Number.isInteger(a) || !Number.isInteger(b)) {
33+
return false;
34+
}
35+
// Valid, return `b`.
36+
if (a === 0) {
37+
return b;
38+
}
39+
return false;
40+
// TODO: Add a case for when both `a` and `b` are positive.
41+
}
42+
43+
/**
44+
* Describes an element based on pieces of a selector.
45+
*/
46+
export class Descriptor {
47+
public rule;
48+
public element;
49+
public combinator = '';
50+
public position = {
51+
explicit: false,
52+
from: 'end' as 'start' | 'end',
53+
index: 1,
54+
type: 'child' as 'child' | 'type'
55+
};
56+
public invalid = false;
57+
private rawContent = '';
58+
59+
constructor (rule: AstRule, content?: string) {
60+
this.rule = rule;
61+
62+
// Create the element.
63+
let tag = 'div';
64+
if (rule.tag?.type === 'TagName') {
65+
tag = rule.tag.name;
66+
} else if (rule.tag?.type === 'WildcardTag') {
67+
this.invalid = true;
68+
}
69+
this.element = document.createElement(tag);
70+
71+
if (this.invalid) {
72+
return;
73+
}
74+
75+
// Set the ids.
76+
if (rule.ids) {
77+
this.element.id = rule.ids.join(' ');
78+
}
79+
80+
// Set the classes.
81+
if (rule.classNames) {
82+
this.element.className = rule.classNames.join(' ');
83+
}
84+
85+
// Set the attributes.
86+
if (rule.attributes) {
87+
for (const attribute of rule.attributes) {
88+
let value = '';
89+
if (attribute.value?.type === 'String') {
90+
value = attribute.value.value;
91+
}
92+
this.element.setAttribute(attribute.name, value);
93+
}
94+
}
95+
96+
// Set the content.
97+
if (content) {
98+
this.content = content;
99+
}
100+
101+
// Set the combinator.
102+
this.combinator = rule.combinator ?? ' ';
103+
104+
// Set the position.
105+
if (rule.pseudoClasses && rule.pseudoClasses.length > 0) {
106+
const pseudoClass = rule.pseudoClasses[0];
107+
this.invalid = this.invalid || rule.pseudoClasses.length > 1 || !positioningPseudoClasses.includes(pseudoClass.name);
108+
if (this.invalid) return;
109+
this.position.explicit = true;
110+
this.position.from = pseudoClass.name.includes('last') ? 'end' : 'start';
111+
this.position.type = pseudoClass.name.includes('type') ? 'type' : 'child';
112+
if (pseudoClass.name.includes('nth')) {
113+
const position = pseudoClass.argument?.type === 'Formula' && parsePositionFormula(pseudoClass.argument.a, pseudoClass.argument.b);
114+
if (position) {
115+
this.position.index = position;
116+
} else {
117+
this.invalid = true;
118+
return;
119+
}
120+
}
121+
}
122+
}
123+
124+
/**
125+
* The content of the element.
126+
*/
127+
public get content (): string {
128+
return this.rawContent;
129+
}
130+
131+
public set content (value: string) {
132+
this.rawContent = value;
133+
// Strip any quote marks from around the content string.
134+
if (/(?:'|")/.test(value.charAt(0)) && /(?:'|")/.test(value.charAt(value.length - 1))) {
135+
value = value.substring(1, value.length - 1);
136+
}
137+
// Place the content in the `href` property of anchor elements.
138+
if (this.element instanceof HTMLAnchorElement) {
139+
this.element.href = value;
140+
}
141+
// Place the content in the `src` property of audio, iframe, image, and video elements.
142+
else if (
143+
this.element instanceof HTMLAudioElement
144+
|| this.element instanceof HTMLIFrameElement
145+
|| this.element instanceof HTMLImageElement
146+
|| this.element instanceof HTMLVideoElement
147+
) {
148+
this.element.src = value;
149+
}
150+
// Place the content in the `placeholder` property of input and textarea elements.
151+
else if (
152+
this.element instanceof HTMLInputElement
153+
|| this.element instanceof HTMLTextAreaElement
154+
) {
155+
this.element.placeholder = value;
156+
}
157+
// Use the content as inner-text and place it in the `value` property of option elements.
158+
else if (this.element instanceof HTMLOptionElement) {
159+
replaceTextNode(this.element, value);
160+
this.element.value = value;
161+
}
162+
// Place the content in the `value` property of select elements.
163+
else if (this.element instanceof HTMLSelectElement) {
164+
this.element.value = value;
165+
}
166+
// Use the content as inner-text for all other elements.
167+
else {
168+
replaceTextNode(this.element, value);
169+
}
170+
}
171+
172+
/**
173+
* A selector string suitable for selecting similar sibling elements.
174+
*/
175+
public get siblingSelector (): string {
176+
let selector = ':scope > ';
177+
selector += this.position.type === 'type' ? this.element.tagName : '*';
178+
selector += ':nth';
179+
selector += this.position.from === 'end' ? '-last' : '';
180+
selector += this.position.type === 'type' ? '-of-type' : '-child';
181+
selector += `(${this.position.index})`;
182+
return selector;
183+
}
184+
}

0 commit comments

Comments
 (0)