( css: string, selector: string, styles: Record<string, string | null>, )
| 115 | * Returns the modified CSS string. Returns `css` unchanged when nothing changed. |
| 116 | */ |
| 117 | export function upsertCssRule( |
| 118 | css: string, |
| 119 | selector: string, |
| 120 | styles: Record<string, string | null>, |
| 121 | ): string { |
| 122 | const normalized = normalizeSelector(selector); |
| 123 | const rules = parseCssRules(css); |
| 124 | const idx = rules.findIndex((r) => normalizeSelector(r.selector) === normalized); |
| 125 | |
| 126 | if (idx !== -1) { |
| 127 | const rule = rules[idx]!; |
| 128 | const decls = parseDeclarations(rule.body); |
| 129 | for (const [prop, value] of Object.entries(styles)) { |
| 130 | if (value === null) { |
| 131 | delete decls[prop]; |
| 132 | } else { |
| 133 | decls[prop] = value; |
| 134 | } |
| 135 | } |
| 136 | const newRuleText = `${rule.selector} {${serializeDeclarations(decls)}}`; |
| 137 | return css.slice(0, rule.start) + newRuleText + css.slice(rule.end); |
| 138 | } |
| 139 | |
| 140 | // Append new rule — skip if all values are null (nothing to write). |
| 141 | const newDecls: Record<string, string> = {}; |
| 142 | for (const [prop, value] of Object.entries(styles)) { |
| 143 | if (value !== null) newDecls[prop] = value; |
| 144 | } |
| 145 | if (!Object.keys(newDecls).length) return css; |
| 146 | |
| 147 | const newRuleText = `${selector} {${serializeDeclarations(newDecls)}}`; |
| 148 | const sep = css.length > 0 && !css.endsWith("\n") ? "\n" : ""; |
| 149 | return css + sep + newRuleText + "\n"; |
| 150 | } |
no test coverage detected