Skip to content

Commit 7322ab4

Browse files
committed
Create the main HTML generator function
Takes a CSS string input, outputs an HTML document.
1 parent 93bab82 commit 7322ab4

File tree

2 files changed

+143
-0
lines changed

2 files changed

+143
-0
lines changed

index.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
export { cssToHtml } from './src/Generator.js';

src/Generator.ts

Lines changed: 142 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,142 @@
1+
/**
2+
* Generate an HTML document from a CSS string.
3+
* @param css The style sheet string.
4+
* @returns An HTML body element containing the generated DOM.
5+
*/
6+
export function cssToHtml(css: string): HTMLBodyElement {
7+
const output = document.createElement('body');
8+
9+
// Parse the CSS string into a CSSOM.
10+
const styleDocument = document.implementation.createHTMLDocument();
11+
const styleElement = document.createElement('style');
12+
styleElement.textContent = css;
13+
styleDocument.body.append(styleElement);
14+
const styleRules = styleElement.sheet?.cssRules;
15+
if (!styleRules) {
16+
return output;
17+
}
18+
19+
// Convert each rule into an HTML element, then add it to the output DOM.
20+
for (const [index, rule] of Object.entries(styleRules) as [string, (CSSStyleRule | CSSMediaRule)][]) {
21+
// Skip:
22+
// - Media rules.
23+
// - Rules starting with `*`.
24+
// - Rules including `:`.
25+
if (
26+
rule instanceof CSSMediaRule
27+
|| rule.selectorText.startsWith('*')
28+
|| rule.selectorText.includes(':')
29+
) {
30+
continue;
31+
}
32+
33+
// Format any combinators in the rule's selector.
34+
let selector = rule.selectorText
35+
.replaceAll(/([\w-])\s+([\w-\.\#])/g, '$1>$2') // Replace child combinator spaces with `>`.
36+
.replaceAll(/>{2,}/g, '>') // Remove excess `>`.
37+
.replaceAll(' ', ''); // Remove excess spaces.
38+
39+
// This object describes an element based on pieces of a selector.
40+
const descriptor = {
41+
previousElement: undefined as HTMLElement | undefined,
42+
previousCharacter: '',
43+
combinator: '',
44+
addressCharacter: '',
45+
classes: [''],
46+
id: '',
47+
tag: '',
48+
add: (character: string): void => {
49+
if (!descriptor.addressCharacter) {
50+
descriptor.tag += character;
51+
}
52+
else if (descriptor.addressCharacter === '.') {
53+
descriptor.classes[descriptor.classes.length - 1] += character;
54+
}
55+
else if (descriptor.addressCharacter === '#') {
56+
descriptor.id += character;
57+
}
58+
59+
descriptor.previousCharacter = character;
60+
},
61+
clear: (): void => {
62+
descriptor.previousCharacter = '';
63+
descriptor.addressCharacter = '';
64+
descriptor.classes = [''];
65+
descriptor.id = '';
66+
descriptor.tag = '';
67+
}
68+
};
69+
70+
function addElementToOutput (): void {
71+
// Create the element.
72+
const newElement = document.createElement(descriptor.tag || 'div');
73+
// Add the classes.
74+
for (const c of descriptor.classes) {
75+
if (c) {
76+
newElement.classList.add(c);
77+
}
78+
}
79+
// Add the ID.
80+
if (descriptor.id) {
81+
newElement.id = descriptor.id;
82+
}
83+
// Add the new element to the DOM.
84+
if (descriptor.previousElement) {
85+
// Child.
86+
if (descriptor.combinator === '>') {
87+
descriptor.previousElement.append(newElement);
88+
}
89+
// Sibling.
90+
else {
91+
descriptor.previousElement.parentElement?.append(newElement);
92+
}
93+
}
94+
else {
95+
output.append(newElement);
96+
}
97+
// Update the descriptor.
98+
descriptor.previousElement = newElement;
99+
}
100+
101+
// For every character in the selector, plus a stop character to indicate the end of the selector.
102+
for (const character of selector + '%') {
103+
// The start of a new selector.
104+
if (!descriptor.previousCharacter) {
105+
if (/(?:\+|~|>)/.test(character)) {
106+
descriptor.combinator = character;
107+
}
108+
else if (character === '.' || character === '#') {
109+
descriptor.addressCharacter = character;
110+
} else {
111+
descriptor.add(character);
112+
}
113+
}
114+
// The character is alphanumeric.
115+
else if (/(?:\w|-)/.test(character)) {
116+
descriptor.add(character);
117+
}
118+
// The character is a dot.
119+
else if (character === '.') {
120+
descriptor.addressCharacter = character;
121+
descriptor.classes.push('');
122+
}
123+
// The character is a hash.
124+
else if (character === '#') {
125+
descriptor.addressCharacter = character;
126+
}
127+
// The character is a combinator.
128+
else if (/(?:\+|~|>)/.test(character)) {
129+
addElementToOutput();
130+
descriptor.clear();
131+
descriptor.combinator = character;
132+
}
133+
// The character none of the above.
134+
else {
135+
addElementToOutput();
136+
descriptor.clear();
137+
}
138+
}
139+
}
140+
141+
return output;
142+
}

0 commit comments

Comments
 (0)