Skip to content

Commit b5bdb55

Browse files
committed
Add ToPlain<T> utility type and typed clone() return types
Each subtype interface now overrides clone() to return ToPlain<T>, which is PlainCSSNode & { type: T['type'] } & { subtype-specific properties }. This means calling .clone() on a narrowed node type gives back a plain object with only the relevant fields typed — no explicit 'as PlainCSSNode' casts needed. AttributeSelector.clone() uses a manually-written return type because attr_operator/attr_flags are stored as numbers on the live node but serialised as strings by clone(). UniversalSelector.name is changed from string|null to string|undefined for consistency with PlainCSSNode's string-field convention. Zero runtime overhead — the change is purely at the TypeScript type level.
1 parent 1db7329 commit b5bdb55

2 files changed

Lines changed: 86 additions & 2 deletions

File tree

src/index.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -57,6 +57,7 @@ export { NODE_TYPES } from './constants'
5757
export {
5858
type CssNodeCommon,
5959
type AnyCss,
60+
type ToPlain,
6061
type StyleSheet,
6162
type Rule,
6263
type Atrule,

src/node-types.ts

Lines changed: 85 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -91,12 +91,45 @@ export interface CssNodeCommon {
9191
clone(options?: CloneOptions): PlainCSSNode
9292
}
9393

94+
/**
95+
* Maps a CssNodeCommon subtype interface to its plain-object equivalent,
96+
* as returned by clone().
97+
*
98+
* The result is always a subtype of PlainCSSNode (the intersection starts
99+
* with PlainCSSNode), with two additions:
100+
* - `type` is narrowed to T's specific literal (enables discriminated unions)
101+
* - subtype-specific properties (those not on CssNodeCommon) are added with
102+
* CssNodeCommon references replaced by PlainCSSNode
103+
*
104+
* Traversal properties (first_child, next_sibling, etc.) are excluded since
105+
* they live on CssNodeCommon and are never serialised by clone().
106+
*
107+
* const rule = root.first_child as Rule
108+
* rule.clone().prelude // PlainCSSNode | null — not PlainCSSNode | undefined
109+
* rule.clone().block // PlainCSSNode | null
110+
*/
111+
export type ToPlain<T extends CssNodeCommon> = PlainCSSNode &
112+
{ type: T['type'] } & {
113+
[K in Exclude<
114+
keyof T,
115+
keyof CssNodeCommon | symbol | 'attr_operator' | 'attr_flags'
116+
> as T[K] extends (...args: any[]) => any ? never : K]: T[K] extends
117+
| CssNodeCommon
118+
| null
119+
| undefined
120+
? PlainCSSNode | Exclude<T[K], CssNodeCommon>
121+
: T[K] extends CssNodeCommon[]
122+
? PlainCSSNode[]
123+
: T[K]
124+
}
125+
94126
// ---------------------------------------------------------------------------
95127
// Structural nodes
96128
// ---------------------------------------------------------------------------
97129

98130
export interface StyleSheet extends CssNodeCommon {
99131
readonly type: typeof STYLESHEET
132+
clone(options?: CloneOptions): ToPlain<StyleSheet>
100133
}
101134

102135
export interface Rule extends CssNodeCommon {
@@ -108,6 +141,7 @@ export interface Rule extends CssNodeCommon {
108141
readonly has_prelude: boolean
109142
readonly has_block: boolean
110143
readonly has_declarations: boolean
144+
clone(options?: CloneOptions): ToPlain<Rule>
111145
}
112146

113147
export interface Atrule extends CssNodeCommon {
@@ -120,6 +154,7 @@ export interface Atrule extends CssNodeCommon {
120154
readonly has_prelude: boolean
121155
readonly has_block: boolean
122156
readonly has_declarations: boolean
157+
clone(options?: CloneOptions): ToPlain<Atrule>
123158
}
124159

125160
export interface Declaration extends CssNodeCommon {
@@ -130,27 +165,33 @@ export interface Declaration extends CssNodeCommon {
130165
readonly value: Value | Raw | null
131166
readonly is_important: boolean
132167
readonly is_browserhack: boolean
168+
clone(options?: CloneOptions): ToPlain<Declaration>
133169
}
134170

135171
export interface Selector extends CssNodeCommon {
136172
readonly type: typeof SELECTOR
173+
clone(options?: CloneOptions): ToPlain<Selector>
137174
}
138175

139176
export interface SelectorList extends CssNodeCommon {
140177
readonly type: typeof SELECTOR_LIST
178+
clone(options?: CloneOptions): ToPlain<SelectorList>
141179
}
142180

143181
export interface Block extends CssNodeCommon {
144182
readonly type: typeof BLOCK
145183
readonly is_empty: boolean
184+
clone(options?: CloneOptions): ToPlain<Block>
146185
}
147186

148187
export interface Comment extends CssNodeCommon {
149188
readonly type: typeof COMMENT
189+
clone(options?: CloneOptions): ToPlain<Comment>
150190
}
151191

152192
export interface Raw extends CssNodeCommon {
153193
readonly type: typeof RAW
194+
clone(options?: CloneOptions): ToPlain<Raw>
154195
}
155196

156197
// ---------------------------------------------------------------------------
@@ -160,26 +201,31 @@ export interface Raw extends CssNodeCommon {
160201
export interface Identifier extends CssNodeCommon {
161202
readonly type: typeof IDENTIFIER
162203
readonly name: string
204+
clone(options?: CloneOptions): ToPlain<Identifier>
163205
}
164206

165207
export interface Number extends CssNodeCommon {
166208
readonly type: typeof NUMBER
167209
readonly value: number
210+
clone(options?: CloneOptions): ToPlain<Number>
168211
}
169212

170213
export interface Dimension extends CssNodeCommon {
171214
readonly type: typeof DIMENSION
172215
readonly value: number
173216
/** Unit string, e.g. "px", "%" */
174217
readonly unit: string
218+
clone(options?: CloneOptions): ToPlain<Dimension>
175219
}
176220

177221
export interface String extends CssNodeCommon {
178222
readonly type: typeof STRING
223+
clone(options?: CloneOptions): ToPlain<String>
179224
}
180225

181226
export interface Hash extends CssNodeCommon {
182227
readonly type: typeof HASH
228+
clone(options?: CloneOptions): ToPlain<Hash>
183229
}
184230

185231
export interface Function extends CssNodeCommon {
@@ -188,30 +234,36 @@ export interface Function extends CssNodeCommon {
188234
readonly name: string
189235
/** Function arguments as raw text, e.g. "255, 0, 0" for rgb(255, 0, 0) */
190236
readonly value: string | null
237+
clone(options?: CloneOptions): ToPlain<Function>
191238
}
192239

193240
export interface Operator extends CssNodeCommon {
194241
readonly type: typeof OPERATOR
195242
/** The operator character(s), e.g. ",", "+", "-" */
196243
readonly value: string
244+
clone(options?: CloneOptions): ToPlain<Operator>
197245
}
198246

199247
export interface Parenthesis extends CssNodeCommon {
200248
readonly type: typeof PARENTHESIS
249+
clone(options?: CloneOptions): ToPlain<Parenthesis>
201250
}
202251

203252
export interface Url extends CssNodeCommon {
204253
readonly type: typeof URL
205254
/** URL content, e.g. '"image.png"' (with quotes) or 'mycursor.cur' (unquoted) */
206255
readonly value: string | null
256+
clone(options?: CloneOptions): ToPlain<Url>
207257
}
208258

209259
export interface UnicodeRange extends CssNodeCommon {
210260
readonly type: typeof UNICODE_RANGE
261+
clone(options?: CloneOptions): ToPlain<UnicodeRange>
211262
}
212263

213264
export interface Value extends CssNodeCommon {
214265
readonly type: typeof VALUE
266+
clone(options?: CloneOptions): ToPlain<Value>
215267
}
216268

217269
// ---------------------------------------------------------------------------
@@ -222,18 +274,21 @@ export interface TypeSelector extends CssNodeCommon {
222274
readonly type: typeof TYPE_SELECTOR
223275
/** Element type, e.g. "div", "span" */
224276
readonly name: string
277+
clone(options?: CloneOptions): ToPlain<TypeSelector>
225278
}
226279

227280
export interface ClassSelector extends CssNodeCommon {
228281
readonly type: typeof CLASS_SELECTOR
229282
/** Class name without dot, e.g. "foo" from ".foo" */
230283
readonly name: string
284+
clone(options?: CloneOptions): ToPlain<ClassSelector>
231285
}
232286

233287
export interface IdSelector extends CssNodeCommon {
234288
readonly type: typeof ID_SELECTOR
235289
/** Id without hash, e.g. "bar" from "#bar" */
236290
readonly name: string
291+
clone(options?: CloneOptions): ToPlain<IdSelector>
237292
}
238293

239294
export interface AttributeSelector extends CssNodeCommon {
@@ -244,40 +299,56 @@ export interface AttributeSelector extends CssNodeCommon {
244299
readonly attr_operator: number
245300
/** One of the ATTR_FLAG_* constants */
246301
readonly attr_flags: number
302+
clone(
303+
options?: CloneOptions,
304+
): PlainCSSNode & {
305+
type: typeof ATTRIBUTE_SELECTOR
306+
name: string
307+
/** Operator as a string, e.g. "=", "~=", "|=" */
308+
attr_operator: string
309+
/** Flags as a string, e.g. "i", "s", or "" */
310+
attr_flags: string
311+
}
247312
}
248313

249314
export interface PseudoClassSelector extends CssNodeCommon {
250315
readonly type: typeof PSEUDO_CLASS_SELECTOR
251316
/** Pseudo-class name without colon, e.g. "hover" */
252317
readonly name: string
318+
clone(options?: CloneOptions): ToPlain<PseudoClassSelector>
253319
}
254320

255321
export interface PseudoElementSelector extends CssNodeCommon {
256322
readonly type: typeof PSEUDO_ELEMENT_SELECTOR
257323
/** Pseudo-element name without colons, e.g. "before" */
258324
readonly name: string
325+
clone(options?: CloneOptions): ToPlain<PseudoElementSelector>
259326
}
260327

261328
export interface Combinator extends CssNodeCommon {
262329
readonly type: typeof COMBINATOR
263330
/** Combinator character(s), e.g. " ", ">", "~", "+", "||", "/deep/" */
264331
readonly name: string
332+
clone(options?: CloneOptions): ToPlain<Combinator>
265333
}
266334

267335
export interface UniversalSelector extends CssNodeCommon {
268336
readonly type: typeof UNIVERSAL_SELECTOR
269-
/** Namespace qualifier (e.g. 'ns' in 'ns|*'), null if no namespace */
270-
readonly name: string | null
337+
/** Namespace qualifier (e.g. 'ns' in 'ns|*'), undefined if no namespace */
338+
readonly name: string | undefined
339+
clone(options?: CloneOptions): ToPlain<UniversalSelector>
271340
}
272341

273342
export interface NestingSelector extends CssNodeCommon {
274343
readonly type: typeof NESTING_SELECTOR
344+
clone(options?: CloneOptions): ToPlain<NestingSelector>
275345
}
276346

277347
export interface NthSelector extends CssNodeCommon {
278348
readonly type: typeof NTH_SELECTOR
279349
readonly nth_a: string | undefined
280350
readonly nth_b: string | undefined
351+
clone(options?: CloneOptions): ToPlain<NthSelector>
281352
}
282353

283354
export interface NthOfSelector extends CssNodeCommon {
@@ -286,10 +357,12 @@ export interface NthOfSelector extends CssNodeCommon {
286357
readonly nth: NthSelector | null
287358
/** The selector list from :nth-child(An+B of <selector>) */
288359
readonly selector: SelectorList | null
360+
clone(options?: CloneOptions): ToPlain<NthOfSelector>
289361
}
290362

291363
export interface LangSelector extends CssNodeCommon {
292364
readonly type: typeof LANG_SELECTOR
365+
clone(options?: CloneOptions): ToPlain<LangSelector>
293366
}
294367

295368
// ---------------------------------------------------------------------------
@@ -298,55 +371,65 @@ export interface LangSelector extends CssNodeCommon {
298371

299372
export interface AtrulePrelude extends CssNodeCommon {
300373
readonly type: typeof AT_RULE_PRELUDE
374+
clone(options?: CloneOptions): ToPlain<AtrulePrelude>
301375
}
302376

303377
export interface MediaQuery extends CssNodeCommon {
304378
readonly type: typeof MEDIA_QUERY
379+
clone(options?: CloneOptions): ToPlain<MediaQuery>
305380
}
306381

307382
export interface MediaFeature extends CssNodeCommon {
308383
readonly type: typeof MEDIA_FEATURE
309384
/** Feature name, e.g. "min-width" */
310385
readonly property: string
386+
clone(options?: CloneOptions): ToPlain<MediaFeature>
311387
}
312388

313389
export interface MediaType extends CssNodeCommon {
314390
readonly type: typeof MEDIA_TYPE
315391
/** Media type text, e.g. "screen", "print" */
316392
readonly value: string
393+
clone(options?: CloneOptions): ToPlain<MediaType>
317394
}
318395

319396
export interface ContainerQuery extends CssNodeCommon {
320397
readonly type: typeof CONTAINER_QUERY
398+
clone(options?: CloneOptions): ToPlain<ContainerQuery>
321399
}
322400

323401
export interface SupportsQuery extends CssNodeCommon {
324402
readonly type: typeof SUPPORTS_QUERY
325403
/** The supports condition text, e.g. "display: flex" from "supports(display: flex)" */
326404
readonly value: string
405+
clone(options?: CloneOptions): ToPlain<SupportsQuery>
327406
}
328407

329408
export interface LayerName extends CssNodeCommon {
330409
readonly type: typeof LAYER_NAME
331410
readonly name: string
332411
/** Alias for name — the layer name string, e.g. "base" from "layer(base)" */
333412
readonly value: string
413+
clone(options?: CloneOptions): ToPlain<LayerName>
334414
}
335415

336416
export interface PreludeSelectorList extends CssNodeCommon {
337417
readonly type: typeof PRELUDE_SELECTORLIST
338418
/** The selector text inside the parentheses, e.g. ".parent" from "(.parent)" */
339419
readonly value: string
420+
clone(options?: CloneOptions): ToPlain<PreludeSelectorList>
340421
}
341422

342423
export interface PreludeOperator extends CssNodeCommon {
343424
readonly type: typeof PRELUDE_OPERATOR
425+
clone(options?: CloneOptions): ToPlain<PreludeOperator>
344426
}
345427

346428
export interface FeatureRange extends CssNodeCommon {
347429
readonly type: typeof FEATURE_RANGE
348430
/** The feature name in a range comparison, e.g. "width" from "(width >= 400px)" */
349431
readonly name: string
432+
clone(options?: CloneOptions): ToPlain<FeatureRange>
350433
}
351434

352435
// ---------------------------------------------------------------------------

0 commit comments

Comments
 (0)