@@ -2,6 +2,7 @@ type Combinator = '>' | '~' | '+';
22
33interface Options {
44 duplicates ?: 'preserve' | 'remove' ;
5+ fill ?: 'fill' | 'no-fill'
56}
67
78/**
@@ -11,7 +12,17 @@ interface Options {
1112 */
1213export function cssToHtml ( css : CSSRuleList | string , options : Options = { } ) : HTMLBodyElement {
1314 const output = document . createElement ( 'body' ) ;
15+ const fillerElements = [ ] as HTMLElement [ ] ;
16+ function isFillerElement ( element : HTMLElement | Element ) : boolean {
17+ for ( const element of fillerElements ) {
18+ if ( element . isSameNode ( element ) ) {
19+ return true ;
20+ }
21+ }
22+ return false ;
23+ }
1424 let styleRules : CSSRuleList | undefined ;
25+
1526 // Parse the CSS string into a CSSOM.
1627 if ( typeof css === 'string' ) {
1728 const styleDocument = document . implementation . createHTMLDocument ( ) ;
@@ -30,13 +41,20 @@ export function cssToHtml(css: CSSRuleList | string, options: Options = {}): HTM
3041 // Convert each rule into an HTML element, then add it to the output DOM.
3142 for ( const [ index , rule ] of Object . entries ( styleRules ) as [ string , ( CSSStyleRule | CSSMediaRule ) ] [ ] ) {
3243 // Skip:
33- // - Media rules.
34- // - Rules including `: `.
35- // - Rules including with `*` .
44+ // - Non-style rules.
45+ // - Rules including `* `.
46+ // - Rules including `:` (that aren't `nth-child` or `nth-of-type`) .
3647 if (
37- || rule . selectorText . includes ( ':' )
3848 ! ( rule instanceof CSSStyleRule )
3949 || rule . selectorText . includes ( '*' )
50+ || ( rule . selectorText . includes ( ':' )
51+ && ! rule . selectorText . includes ( ':first-child' )
52+ && ! rule . selectorText . includes ( ':nth-child' )
53+ && ! rule . selectorText . includes ( ':last-child' )
54+ && ! rule . selectorText . includes ( ':first-of-type' )
55+ && ! rule . selectorText . includes ( ':nth-of-type' )
56+ && ! rule . selectorText . includes ( ':last-of-type' )
57+ )
4058 ) {
4159 continue ;
4260 }
@@ -56,6 +74,10 @@ export function cssToHtml(css: CSSRuleList | string, options: Options = {}): HTM
5674 classes : [ '' ] ,
5775 id : '' ,
5876 tag : '' ,
77+ position : {
78+ type : '' as 'child' | 'type' ,
79+ index : 0
80+ } ,
5981 add : ( character : string ) : void => {
6082 if ( ! descriptor . addressCharacter ) {
6183 descriptor . tag += character ;
@@ -135,14 +157,98 @@ export function cssToHtml(css: CSSRuleList | string, options: Options = {}): HTM
135157 descriptor . previousElement = newElement ;
136158 }
137159
160+ function addElementToOutputWithFill ( childType : 'child' | 'type' , fillType : 'first' | 'nth' | 'last' , fillAmount : number ) : void {
161+ const desiredIndex = fillAmount - 1 ;
162+
163+ // Create the new element.
164+ const newElement = document . createElement ( descriptor . tag || 'div' ) ;
165+ // Add the classes.
166+ for ( const c of descriptor . classes ) {
167+ ( c && newElement . classList . add ( c ) ) ;
168+ }
169+ // Add the ID.
170+ if ( descriptor . id ) {
171+ newElement . id = descriptor . id ;
172+ }
173+
174+ // Get a reference to the parent element.
175+ let parentElement = undefined as HTMLElement | undefined ;
176+ if ( descriptor . previousElement ) {
177+ if ( descriptor . combinator === '>' ) {
178+ parentElement = descriptor . previousElement ;
179+ } else {
180+ parentElement = descriptor . previousElement . parentElement ?? output ;
181+ }
182+ } else {
183+ parentElement = output ;
184+ }
185+
186+ if ( ! parentElement ) {
187+ return ;
188+ }
189+
190+ // Update the descriptor.
191+ descriptor . previousElement = newElement ;
192+
193+ if ( fillType === 'first' ) {
194+ parentElement . prepend ( newElement ) ;
195+ return ;
196+ }
197+
198+ if ( fillType === 'last' ) {
199+ parentElement . append ( newElement ) ;
200+ return ;
201+ }
202+
203+ // Check if there is a sibling element in the desired position.
204+ const blockingSibling = parentElement . querySelector ( childType === 'type' ? `${ newElement . tagName } :nth-of-type(${ fillAmount } )` : `:nth-child(${ fillAmount } )` ) ;
205+ if ( blockingSibling ) {
206+ parentElement . insertBefore ( newElement , blockingSibling ) ;
207+ if ( isFillerElement ( blockingSibling ) ) {
208+ blockingSibling . remove ( ) ;
209+ }
210+ return ;
211+ }
212+
213+ // Add the element to the DOM.
214+ parentElement . append ( newElement ) ;
215+
216+ if ( options . fill !== 'no-fill' ) {
217+ // Count the previous siblings.
218+ let previousSiblings = 0 ;
219+ let previousSibling = newElement . previousElementSibling ;
220+ while ( previousSibling && previousSiblings < desiredIndex ) {
221+ previousSibling = previousSibling ?. previousElementSibling ;
222+ if ( childType === 'type' && previousSibling ?. tagName !== newElement . tagName ) {
223+ continue ;
224+ }
225+ previousSiblings ++ ;
226+ }
227+
228+ // Fill duplicate elements up to the desired position.
229+ const duplicatesRequired = desiredIndex - previousSiblings ;
230+ for ( let i = 0 ; i < duplicatesRequired ; i ++ ) {
231+ // Create the duplicate element.
232+ const duplicateElement = newElement . cloneNode ( ) as HTMLElement ;
233+ // Remove the ID.
234+ duplicateElement . removeAttribute ( 'id' ) ;
235+ // Make a note of the duplicate element.
236+ fillerElements . push ( duplicateElement ) ;
237+ // Add the duplicate element to the DOM.
238+ parentElement . insertBefore ( duplicateElement , newElement ) ;
239+ }
240+ }
241+ }
242+
138243 // For every character in the selector, plus a stop character to indicate the end of the selector.
139- for ( const character of selector + '%' ) {
244+ selector += '%' ;
245+ for ( let i = 0 ; i < selector . length ; i ++ ) {
246+ const character = selector [ i ] ;
140247 // The start of a new selector.
141248 if ( ! descriptor . previousCharacter ) {
142249 if ( / (?: \+ | ~ | > ) / . test ( character ) ) {
143250 descriptor . combinator = character as Combinator ;
144- }
145- else if ( character === '.' || character === '#' ) {
251+ } else if ( character === '.' || character === '#' ) {
146252 descriptor . addressCharacter = character ;
147253 } else {
148254 descriptor . add ( character ) ;
@@ -167,7 +273,22 @@ export function cssToHtml(css: CSSRuleList | string, options: Options = {}): HTM
167273 descriptor . clear ( ) ;
168274 descriptor . combinator = character as Combinator ;
169275 }
170- // The character none of the above.
276+ // The character is a colon.
277+ else if ( character === ':' ) {
278+ const nthSelector = selector . substring ( i + 1 ) ;
279+ const pseudoSelector =
280+ / ^ ( f i r s t - c h i l d | f i r s t - o f - t y p e ) / i. exec ( nthSelector )
281+ ?? / ^ ( n t h - c h i l d | n t h - o f - t y p e ) \( ( [ 0 - 9 ] + ) \) / i. exec ( nthSelector )
282+ ?? / ^ ( l a s t - c h i l d | l a s t - o f - t y p e ) / i. exec ( nthSelector ) ;
283+ if ( pseudoSelector ) {
284+ const childType = pseudoSelector [ 1 ] . includes ( 'type' ) ? 'type' : 'child' ;
285+ const fillType = pseudoSelector [ 1 ] . split ( '-' ) [ 0 ] as 'first' | 'nth' | 'last' ;
286+ addElementToOutputWithFill ( childType , fillType , parseInt ( pseudoSelector [ 2 ] ) || 0 ) ;
287+ i += pseudoSelector [ 0 ] . length ;
288+ }
289+ descriptor . clear ( ) ;
290+ }
291+ // The character is none of the above.
171292 else {
172293 addElementToOutput ( ) ;
173294 descriptor . clear ( ) ;
0 commit comments