@elastic/distillate 0.0.0 → 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (285) hide show
  1. package/LICENSE.txt +93 -0
  2. package/NOTICE.txt +30 -0
  3. package/README.md +142 -0
  4. package/THIRD_PARTY_LICENSES.md +768 -0
  5. package/dist/cjs/collector.js +420 -0
  6. package/dist/cjs/collector.js.map +1 -0
  7. package/dist/cjs/css_scan.js +186 -0
  8. package/dist/cjs/css_scan.js.map +1 -0
  9. package/dist/cjs/dom_sink.js +40 -0
  10. package/dist/cjs/dom_sink.js.map +1 -0
  11. package/dist/cjs/emotion.js +189 -0
  12. package/dist/cjs/emotion.js.map +1 -0
  13. package/dist/cjs/engine.js +79 -0
  14. package/dist/cjs/engine.js.map +1 -0
  15. package/dist/cjs/environment.js +9 -0
  16. package/dist/cjs/environment.js.map +1 -0
  17. package/dist/cjs/hash.js +86 -0
  18. package/dist/cjs/hash.js.map +1 -0
  19. package/dist/cjs/idents.js +23 -0
  20. package/dist/cjs/idents.js.map +1 -0
  21. package/dist/cjs/index.js +42 -0
  22. package/dist/cjs/index.js.map +1 -0
  23. package/dist/cjs/instance.js +34 -0
  24. package/dist/cjs/instance.js.map +1 -0
  25. package/dist/cjs/local_vars.js +100 -0
  26. package/dist/cjs/local_vars.js.map +1 -0
  27. package/dist/cjs/minify.js +77 -0
  28. package/dist/cjs/minify.js.map +1 -0
  29. package/dist/cjs/names.js +77 -0
  30. package/dist/cjs/names.js.map +1 -0
  31. package/dist/cjs/nesting.js +406 -0
  32. package/dist/cjs/nesting.js.map +1 -0
  33. package/dist/cjs/package.json +3 -0
  34. package/dist/cjs/runtime.js +257 -0
  35. package/dist/cjs/runtime.js.map +1 -0
  36. package/dist/cjs/styles/authoring.js +169 -0
  37. package/dist/cjs/styles/authoring.js.map +1 -0
  38. package/dist/cjs/styles/declarations.js +139 -0
  39. package/dist/cjs/styles/declarations.js.map +1 -0
  40. package/dist/cjs/styles/index.js +32 -0
  41. package/dist/cjs/styles/index.js.map +1 -0
  42. package/dist/cjs/styles/module.js +233 -0
  43. package/dist/cjs/styles/module.js.map +1 -0
  44. package/dist/cjs/styles/registry.js +187 -0
  45. package/dist/cjs/styles/registry.js.map +1 -0
  46. package/dist/cjs/styles/types.js +9 -0
  47. package/dist/cjs/styles/types.js.map +1 -0
  48. package/dist/cjs/testing.js +37 -0
  49. package/dist/cjs/testing.js.map +1 -0
  50. package/dist/cjs/theme.js +249 -0
  51. package/dist/cjs/theme.js.map +1 -0
  52. package/dist/cjs/tokens/contextual_var.js +39 -0
  53. package/dist/cjs/tokens/contextual_var.js.map +1 -0
  54. package/dist/cjs/tokens/index.js +24 -0
  55. package/dist/cjs/tokens/index.js.map +1 -0
  56. package/dist/cjs/tokens/kind.js +14 -0
  57. package/dist/cjs/tokens/kind.js.map +1 -0
  58. package/dist/cjs/tokens/light_dark.js +42 -0
  59. package/dist/cjs/tokens/light_dark.js.map +1 -0
  60. package/dist/cjs/tokens/scale_token.js +28 -0
  61. package/dist/cjs/tokens/scale_token.js.map +1 -0
  62. package/dist/cjs/tokens/theme_token.js +28 -0
  63. package/dist/cjs/tokens/theme_token.js.map +1 -0
  64. package/dist/cjs/var_invariant.js +115 -0
  65. package/dist/cjs/var_invariant.js.map +1 -0
  66. package/dist/collector.d.ts +84 -0
  67. package/dist/collector.d.ts.map +1 -0
  68. package/dist/collector.js +416 -0
  69. package/dist/collector.js.map +1 -0
  70. package/dist/css_scan.d.ts +57 -0
  71. package/dist/css_scan.d.ts.map +1 -0
  72. package/dist/css_scan.js +180 -0
  73. package/dist/css_scan.js.map +1 -0
  74. package/dist/dom_sink.d.ts +27 -0
  75. package/dist/dom_sink.d.ts.map +1 -0
  76. package/dist/dom_sink.js +36 -0
  77. package/dist/dom_sink.js.map +1 -0
  78. package/dist/emotion.d.ts +47 -0
  79. package/dist/emotion.d.ts.map +1 -0
  80. package/dist/emotion.js +184 -0
  81. package/dist/emotion.js.map +1 -0
  82. package/dist/engine.d.ts +41 -0
  83. package/dist/engine.d.ts.map +1 -0
  84. package/dist/engine.js +75 -0
  85. package/dist/engine.js.map +1 -0
  86. package/dist/environment.d.ts +43 -0
  87. package/dist/environment.d.ts.map +1 -0
  88. package/dist/environment.js +8 -0
  89. package/dist/environment.js.map +1 -0
  90. package/dist/hash.d.ts +2 -0
  91. package/dist/hash.d.ts.map +1 -0
  92. package/dist/hash.js +82 -0
  93. package/dist/hash.js.map +1 -0
  94. package/dist/idents.d.ts +9 -0
  95. package/dist/idents.d.ts.map +1 -0
  96. package/dist/idents.js +19 -0
  97. package/dist/idents.js.map +1 -0
  98. package/dist/index.d.ts +9 -0
  99. package/dist/index.d.ts.map +1 -0
  100. package/dist/index.js +14 -0
  101. package/dist/index.js.map +1 -0
  102. package/dist/instance.d.ts +16 -0
  103. package/dist/instance.d.ts.map +1 -0
  104. package/dist/instance.js +30 -0
  105. package/dist/instance.js.map +1 -0
  106. package/dist/local_vars.d.ts +75 -0
  107. package/dist/local_vars.d.ts.map +1 -0
  108. package/dist/local_vars.js +92 -0
  109. package/dist/local_vars.js.map +1 -0
  110. package/dist/minify.d.ts +11 -0
  111. package/dist/minify.d.ts.map +1 -0
  112. package/dist/minify.js +73 -0
  113. package/dist/minify.js.map +1 -0
  114. package/dist/names.d.ts +39 -0
  115. package/dist/names.d.ts.map +1 -0
  116. package/dist/names.js +70 -0
  117. package/dist/names.js.map +1 -0
  118. package/dist/nesting.d.ts +59 -0
  119. package/dist/nesting.d.ts.map +1 -0
  120. package/dist/nesting.js +400 -0
  121. package/dist/nesting.js.map +1 -0
  122. package/dist/runtime.d.ts +36 -0
  123. package/dist/runtime.d.ts.map +1 -0
  124. package/dist/runtime.js +252 -0
  125. package/dist/runtime.js.map +1 -0
  126. package/dist/styles/authoring.d.ts +59 -0
  127. package/dist/styles/authoring.d.ts.map +1 -0
  128. package/dist/styles/authoring.js +155 -0
  129. package/dist/styles/authoring.js.map +1 -0
  130. package/dist/styles/declarations.d.ts +10 -0
  131. package/dist/styles/declarations.d.ts.map +1 -0
  132. package/dist/styles/declarations.js +132 -0
  133. package/dist/styles/declarations.js.map +1 -0
  134. package/dist/styles/index.d.ts +7 -0
  135. package/dist/styles/index.d.ts.map +1 -0
  136. package/dist/styles/index.js +12 -0
  137. package/dist/styles/index.js.map +1 -0
  138. package/dist/styles/module.d.ts +5 -0
  139. package/dist/styles/module.d.ts.map +1 -0
  140. package/dist/styles/module.js +229 -0
  141. package/dist/styles/module.js.map +1 -0
  142. package/dist/styles/registry.d.ts +48 -0
  143. package/dist/styles/registry.d.ts.map +1 -0
  144. package/dist/styles/registry.js +182 -0
  145. package/dist/styles/registry.js.map +1 -0
  146. package/dist/styles/types.d.ts +211 -0
  147. package/dist/styles/types.d.ts.map +1 -0
  148. package/dist/styles/types.js +8 -0
  149. package/dist/styles/types.js.map +1 -0
  150. package/dist/testing.d.ts +10 -0
  151. package/dist/testing.d.ts.map +1 -0
  152. package/dist/testing.js +31 -0
  153. package/dist/testing.js.map +1 -0
  154. package/dist/theme.d.ts +85 -0
  155. package/dist/theme.d.ts.map +1 -0
  156. package/dist/theme.js +240 -0
  157. package/dist/theme.js.map +1 -0
  158. package/dist/tokens/contextual_var.d.ts +35 -0
  159. package/dist/tokens/contextual_var.d.ts.map +1 -0
  160. package/dist/tokens/contextual_var.js +33 -0
  161. package/dist/tokens/contextual_var.js.map +1 -0
  162. package/dist/tokens/index.d.ts +5 -0
  163. package/dist/tokens/index.d.ts.map +1 -0
  164. package/dist/tokens/index.js +11 -0
  165. package/dist/tokens/index.js.map +1 -0
  166. package/dist/tokens/kind.d.ts +2 -0
  167. package/dist/tokens/kind.d.ts.map +1 -0
  168. package/dist/tokens/kind.js +10 -0
  169. package/dist/tokens/kind.js.map +1 -0
  170. package/dist/tokens/light_dark.d.ts +20 -0
  171. package/dist/tokens/light_dark.d.ts.map +1 -0
  172. package/dist/tokens/light_dark.js +37 -0
  173. package/dist/tokens/light_dark.js.map +1 -0
  174. package/dist/tokens/scale_token.d.ts +20 -0
  175. package/dist/tokens/scale_token.d.ts.map +1 -0
  176. package/dist/tokens/scale_token.js +23 -0
  177. package/dist/tokens/scale_token.js.map +1 -0
  178. package/dist/tokens/theme_token.d.ts +20 -0
  179. package/dist/tokens/theme_token.d.ts.map +1 -0
  180. package/dist/tokens/theme_token.js +23 -0
  181. package/dist/tokens/theme_token.js.map +1 -0
  182. package/dist/var_invariant.d.ts +25 -0
  183. package/dist/var_invariant.d.ts.map +1 -0
  184. package/dist/var_invariant.js +108 -0
  185. package/dist/var_invariant.js.map +1 -0
  186. package/docs/concepts/authoring.md +115 -0
  187. package/docs/concepts/collection.md +66 -0
  188. package/docs/concepts/distillery.md +47 -0
  189. package/docs/concepts/index.md +18 -0
  190. package/docs/concepts/naming-and-output.md +123 -0
  191. package/docs/concepts/ordering.md +61 -0
  192. package/docs/concepts/single-copy.md +103 -0
  193. package/docs/concepts/tokens.md +99 -0
  194. package/docs/docset.yml +53 -0
  195. package/docs/examples/01-stylesheet.ts +20 -0
  196. package/docs/examples/02-artifact.ts +26 -0
  197. package/docs/examples/03-variants.ts +40 -0
  198. package/docs/examples/04-emotion.ts +33 -0
  199. package/docs/examples/05-var-invariant.ts +31 -0
  200. package/docs/examples/06-payload.ts +85 -0
  201. package/docs/examples/fixture.ts +22 -0
  202. package/docs/getting-started/index.md +14 -0
  203. package/docs/getting-started/installation.md +30 -0
  204. package/docs/getting-started/quick-start.md +61 -0
  205. package/docs/getting-started/your-first-stylesheet.md +57 -0
  206. package/docs/guides/compact-artifacts.md +87 -0
  207. package/docs/guides/emotion-migration.md +60 -0
  208. package/docs/guides/index.md +17 -0
  209. package/docs/guides/name-collisions.md +37 -0
  210. package/docs/guides/non-css-surfaces.md +73 -0
  211. package/docs/guides/react-renderer.md +158 -0
  212. package/docs/guides/theming.md +107 -0
  213. package/docs/guides/var-invariant.md +24 -0
  214. package/docs/guides/vs-emotion.md +61 -0
  215. package/docs/index.md +73 -0
  216. package/docs/logo.svg +7 -0
  217. package/docs/playground/index.html +12 -0
  218. package/docs/playground/package.json +23 -0
  219. package/docs/playground/src/app/app.tsx +109 -0
  220. package/docs/playground/src/app/css_panel.tsx +119 -0
  221. package/docs/playground/src/app/editor_panel.tsx +87 -0
  222. package/docs/playground/src/app/index.ts +8 -0
  223. package/docs/playground/src/app/monaco_css_editor.tsx +84 -0
  224. package/docs/playground/src/app/monaco_source_editor.tsx +132 -0
  225. package/docs/playground/src/app/preview_panel.tsx +99 -0
  226. package/docs/playground/src/app/tokens_panel.tsx +117 -0
  227. package/docs/playground/src/app/types.ts +10 -0
  228. package/docs/playground/src/examples/snippets.test.ts +28 -0
  229. package/docs/playground/src/examples/snippets.ts +418 -0
  230. package/docs/playground/src/lib/compile.test.ts +157 -0
  231. package/docs/playground/src/lib/compile.ts +242 -0
  232. package/docs/playground/src/lib/demo_environment.test.ts +123 -0
  233. package/docs/playground/src/lib/demo_environment.ts +140 -0
  234. package/docs/playground/src/lib/editor_lib.test.ts +39 -0
  235. package/docs/playground/src/lib/editor_lib.ts +142 -0
  236. package/docs/playground/src/lib/format_css.test.ts +41 -0
  237. package/docs/playground/src/lib/format_css.ts +71 -0
  238. package/docs/playground/src/main.tsx +25 -0
  239. package/docs/playground/src/setup/chrome.test.ts +47 -0
  240. package/docs/playground/src/setup/chrome.ts +365 -0
  241. package/docs/playground/src/setup/monaco_env.ts +64 -0
  242. package/docs/playground/tsconfig.json +22 -0
  243. package/docs/playground/vite.config.ts +37 -0
  244. package/docs/playground/vitest.config.ts +35 -0
  245. package/docs/playground.md +28 -0
  246. package/docs/reference/api/emotion.md +372 -0
  247. package/docs/reference/api/index-1.md +2180 -0
  248. package/docs/reference/api/index.md +9 -0
  249. package/docs/reference/api/testing.md +92 -0
  250. package/docs/reference/api.md +12 -0
  251. package/docs/reference/entry-points.md +125 -0
  252. package/docs/reference/environment.md +60 -0
  253. package/docs/reference/index.md +11 -0
  254. package/docs/reference/okf-map.md +92 -0
  255. package/package.json +127 -4
  256. package/src/collector.ts +526 -0
  257. package/src/css_scan.ts +263 -0
  258. package/src/dom_sink.ts +65 -0
  259. package/src/emotion.ts +311 -0
  260. package/src/engine.ts +191 -0
  261. package/src/environment.ts +56 -0
  262. package/src/hash.ts +111 -0
  263. package/src/idents.ts +21 -0
  264. package/src/index.ts +93 -0
  265. package/src/instance.ts +49 -0
  266. package/src/local_vars.ts +220 -0
  267. package/src/minify.ts +87 -0
  268. package/src/names.ts +129 -0
  269. package/src/nesting.ts +596 -0
  270. package/src/runtime.ts +428 -0
  271. package/src/styles/authoring.ts +244 -0
  272. package/src/styles/declarations.ts +172 -0
  273. package/src/styles/index.ts +58 -0
  274. package/src/styles/module.ts +396 -0
  275. package/src/styles/registry.ts +261 -0
  276. package/src/styles/types.ts +283 -0
  277. package/src/testing.ts +48 -0
  278. package/src/theme.ts +454 -0
  279. package/src/tokens/contextual_var.ts +72 -0
  280. package/src/tokens/index.ts +17 -0
  281. package/src/tokens/kind.ts +13 -0
  282. package/src/tokens/light_dark.ts +58 -0
  283. package/src/tokens/scale_token.ts +41 -0
  284. package/src/tokens/theme_token.ts +42 -0
  285. package/src/var_invariant.ts +147 -0
package/src/nesting.ts ADDED
@@ -0,0 +1,596 @@
1
+ /*
2
+ * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
3
+ * or more contributor license agreements. Licensed under the Elastic License
4
+ * 2.0; you may not use this file except in compliance with the Elastic License
5
+ * 2.0.
6
+ */
7
+
8
+ // Stylis-based flattener for Emotion-style nested `css` templates.
9
+ //
10
+ // Nesting semantics (`&` substitution, comma cross-products, descendant
11
+ // defaulting, `@media` hoisting) are owned entirely by stylis. We serialize
12
+ // the authored `(strings, values)` into a sentinel-carrying CSS string, hand
13
+ // it to `compile()`, then walk the element tree and re-attach the live
14
+ // interpolated values by matching sentinels.
15
+ //
16
+ // The walk is pinned to stylis 4.4.0; `nesting_contract.test.ts` asserts the
17
+ // `compile()` tree shape (`props` vs `value`, `@media` children, DECL text).
18
+
19
+ import { compile, type Element } from 'stylis';
20
+
21
+ import { scanCss } from './css_scan';
22
+ import { isLocalVarMarker } from './local_vars';
23
+ import { isLocalVarRef } from './local_vars';
24
+ import type { StyleHandle } from './styles/types';
25
+ import {
26
+ isContextualCssVar,
27
+ isContextualCssVarName,
28
+ isCssToken,
29
+ isScaleToken,
30
+ } from './tokens';
31
+
32
+ /** Rebuilt template fragment in tagged-template shape (`strings.length === values.length + 1`). */
33
+ export interface TemplateSlice {
34
+ /** Literal pieces between interpolations. */
35
+ readonly strings: readonly string[];
36
+ /** Interpolated values, one per hole. */
37
+ readonly values: readonly unknown[];
38
+ }
39
+
40
+ /** `&`-relative selector fragment: literal text or a cross-class {@link StyleHandle}. */
41
+ export type SelectorPart = string | StyleHandle;
42
+ /** One comma alternative; parts concatenate into a selector. */
43
+ export type SelectorAlternative = readonly SelectorPart[];
44
+
45
+ /** Flattened nested rule: selector alternatives plus declaration slice. */
46
+ export interface FlattenedRule {
47
+ /** Discriminant for {@link FlattenedEntry}. */
48
+ readonly kind: 'rule';
49
+ /** Comma alternatives relative to the parent handle. */
50
+ readonly alternatives: readonly SelectorAlternative[];
51
+ /** Declaration template for this rule. */
52
+ readonly slice: TemplateSlice;
53
+ }
54
+
55
+ /** Flattened `@media` block: query, optional self declarations, nested rules. */
56
+ export interface FlattenedMedia {
57
+ /** Discriminant for {@link FlattenedEntry}. */
58
+ readonly kind: 'media';
59
+ /** Media query text. */
60
+ readonly query: string;
61
+ /** Bare declarations inside the block, applied to the self handle. */
62
+ readonly self?: TemplateSlice;
63
+ /** Nested `&` rules inside this media block. */
64
+ readonly rules: readonly FlattenedRule[];
65
+ }
66
+
67
+ /** Nested rule or `@media` produced by flattening. */
68
+ export type FlattenedEntry = FlattenedRule | FlattenedMedia;
69
+
70
+ /** Flattened `css` template: self declarations plus nested entries. */
71
+ export interface FlattenedTemplate {
72
+ /** Self-handle declarations. `null` in global mode. */
73
+ readonly self: TemplateSlice | null;
74
+ /** Sibling nested rules and media blocks. */
75
+ readonly nested: readonly FlattenedEntry[];
76
+ }
77
+
78
+ /** `self` keeps a host handle; `global` does not. */
79
+ export type FlattenMode = 'self' | 'global';
80
+
81
+ /** Mode and optional inlining hook for {@link flattenTemplate}. */
82
+ export interface FlattenOptions {
83
+ /** Whether the template authors a host handle or global rules. */
84
+ readonly mode: FlattenMode;
85
+ /**
86
+ * Optional splice of a value's source slice before compilation.
87
+ * Return `null`/`undefined` to serialize the value as a sentinel.
88
+ */
89
+ readonly inlineValue?: (
90
+ value: unknown,
91
+ position: InterpolationPosition
92
+ ) => TemplateSlice | null | undefined;
93
+ }
94
+
95
+ /** Where an interpolation sits in the serialized template. */
96
+ export type InterpolationPosition = 'selector' | 'statement';
97
+
98
+ const SELF_SENTINEL = '.__dstl_self__';
99
+
100
+ // Brace presence is the trigger for the nesting path. Brace-free templates
101
+ // keep the byte-identical fast path in `styles/authoring.ts`. Quoted braces (`content:
102
+ // "{"`) intentionally route here too — they used to throw.
103
+ export const hasNestedSyntax = (strings: readonly string[]): boolean =>
104
+ strings.some((part) => part.includes('{') || part.includes('}'));
105
+
106
+ export const flattenTemplate = (
107
+ strings: readonly string[],
108
+ values: readonly unknown[],
109
+ options: FlattenOptions
110
+ ): FlattenedTemplate => {
111
+ assertNoReservedPatterns(strings);
112
+ const inlined = options.inlineValue
113
+ ? inlineTemplate(strings, values, options.inlineValue)
114
+ : { strings, values };
115
+ const { mode } = options;
116
+ const { text, sentinelValues } = serialize(
117
+ inlined.strings,
118
+ inlined.values,
119
+ mode
120
+ );
121
+ scanStructure(text, mode);
122
+ const tree = compile(canonicalizeAtMedia(text));
123
+ return walk(tree, mode, sentinelValues);
124
+ };
125
+
126
+ // --- Serialization -------------------------------------------------------
127
+
128
+ interface Serialized {
129
+ /** Sentinel-carrying CSS string handed to stylis. */
130
+ readonly text: string;
131
+ /** Sentinel index → the live interpolated value. */
132
+ readonly sentinelValues: readonly unknown[];
133
+ }
134
+
135
+ const serialize = (
136
+ strings: readonly string[],
137
+ values: readonly unknown[],
138
+ mode: FlattenMode
139
+ ): Serialized => {
140
+ let text = mode === 'self' ? `${SELF_SENTINEL}{` : '';
141
+ strings.forEach((part, index) => {
142
+ text += part;
143
+ if (index >= values.length) {
144
+ return;
145
+ }
146
+ const value = values[index];
147
+ if (isLocalVarMarker(value)) {
148
+ // Property-name sentinel; stylis DECL `.props` is the ident (`--dstl-N`).
149
+ text += `--dstl-${index}:0;`;
150
+ return;
151
+ }
152
+ if (isSerializableScalar(value) || isHandleLike(value)) {
153
+ // Identifier sentinel in selectors and values (`__dstlN__`).
154
+ text += `__dstl${index}__`;
155
+ return;
156
+ }
157
+ throw new Error(
158
+ `Unsupported interpolation in css template: ${describe(value)}. Interpolate a theme token, scale token, contextual var, local var (ref/marker), string, number, or a style handle in selector position.`
159
+ );
160
+ });
161
+ if (mode === 'self') {
162
+ text += '}';
163
+ }
164
+ return { text, sentinelValues: values };
165
+ };
166
+
167
+ // --- Pre-compile validation ---------------------------------------------
168
+
169
+ const RESERVED_PATTERNS: readonly RegExp[] = [
170
+ /__dstl_self__/,
171
+ /__dstl\d+__/,
172
+ /--dstl-\d+/,
173
+ ];
174
+
175
+ const assertNoReservedPatterns = (strings: readonly string[]): void => {
176
+ for (const part of strings) {
177
+ for (const pattern of RESERVED_PATTERNS) {
178
+ if (pattern.test(part)) {
179
+ throw new Error(
180
+ `css template contains reserved flattener syntax (${pattern.source}); rename the offending identifier.`
181
+ );
182
+ }
183
+ }
184
+ }
185
+ };
186
+
187
+ // Scan-only structural validator over the serialized text. Classifies, never
188
+ // restructures — stylis owns all flattening. Enforces v1 scope before stylis
189
+ // can normalize, hoist, or silently drop malformed constructs.
190
+ const scanStructure = (text: string, mode: FlattenMode): void => {
191
+ let depth = 0;
192
+ let parenDepth = 0;
193
+ let mediaDepth = 0;
194
+ const mediaBraceStack: number[] = [];
195
+ let quote: string | null = null;
196
+ let inComment = false;
197
+
198
+ for (let i = 0; i < text.length; i += 1) {
199
+ const ch = text[i];
200
+ if (inComment) {
201
+ if (ch === '*' && text[i + 1] === '/') {
202
+ inComment = false;
203
+ i += 1;
204
+ }
205
+ continue;
206
+ }
207
+ if (quote) {
208
+ if (ch === '\\') {
209
+ i += 1;
210
+ } else if (ch === quote) {
211
+ quote = null;
212
+ }
213
+ continue;
214
+ }
215
+ if (ch === '/' && text[i + 1] === '*') {
216
+ inComment = true;
217
+ i += 1;
218
+ continue;
219
+ }
220
+ if (ch === '"' || ch === "'") {
221
+ quote = ch;
222
+ continue;
223
+ }
224
+ if (ch === '(') {
225
+ parenDepth += 1;
226
+ continue;
227
+ }
228
+ if (ch === ')') {
229
+ parenDepth = Math.max(0, parenDepth - 1);
230
+ continue;
231
+ }
232
+ if (parenDepth > 0) {
233
+ continue;
234
+ }
235
+ if (ch === '@') {
236
+ const rest = text.slice(i + 1);
237
+ const name = /^[a-zA-Z-]+/.exec(rest)?.[0] ?? '';
238
+ if (name.toLowerCase() !== 'media') {
239
+ throw new Error(
240
+ `Only @media is supported in css templates; found @${name || '(unknown)'}. (@supports, @container, @keyframes, @font-face, and others are follow-ups.)`
241
+ );
242
+ }
243
+ if (mediaDepth > 0) {
244
+ throw new Error('Nested @media inside @media is not supported.');
245
+ }
246
+ mediaDepth += 1;
247
+ mediaBraceStack.push(depth);
248
+ continue;
249
+ }
250
+ if (ch === '{') {
251
+ depth += 1;
252
+ continue;
253
+ }
254
+ if (ch === '}') {
255
+ depth = Math.max(0, depth - 1);
256
+ if (
257
+ mediaDepth > 0 &&
258
+ mediaBraceStack.length > 0 &&
259
+ depth === mediaBraceStack[mediaBraceStack.length - 1]
260
+ ) {
261
+ mediaBraceStack.pop();
262
+ mediaDepth -= 1;
263
+ }
264
+ continue;
265
+ }
266
+ if (mode === 'global' && depth === 0) {
267
+ if (ch === ';') {
268
+ throw new Error(
269
+ 'injectGlobal templates cannot have top-level declarations; wrap them in a selector.'
270
+ );
271
+ }
272
+ if (ch === '&') {
273
+ throw new Error(
274
+ 'injectGlobal templates cannot use `&` at the top level; global styles have no self handle.'
275
+ );
276
+ }
277
+ }
278
+ }
279
+
280
+ if (depth !== 0 || quote || inComment) {
281
+ throw new Error('css template has unbalanced braces, quotes, or comments.');
282
+ }
283
+ };
284
+
285
+ /** stylis only wraps bare in-media declarations for lowercase `@media`. */
286
+ // stylis matches at-rules case-sensitively, so `@MEDIA` has to be normalized
287
+ // before it parses. Only `text` spans are eligible: an `@media` inside a
288
+ // string, a comment, or an unquoted `url(...)` is data, and rewriting the last
289
+ // of those silently changes a case-sensitive path.
290
+ const canonicalizeAtMedia = (text: string): string =>
291
+ scanCss(text)
292
+ .map((span) =>
293
+ span.kind === 'text'
294
+ ? // A `\\b` here would also match `@media-foo`, rewriting an unknown
295
+ // at-rule whose name merely starts with `media`.
296
+ span.text.replace(/@media(?![\w-])/gi, '@media')
297
+ : span.text
298
+ )
299
+ .join('');
300
+
301
+ // --- Walk + re-attach ----------------------------------------------------
302
+
303
+ const walk = (
304
+ tree: readonly Element[],
305
+ mode: FlattenMode,
306
+ values: readonly unknown[]
307
+ ): FlattenedTemplate => {
308
+ const selfBuilder = new SliceBuilder(values);
309
+ let selfSeen = false;
310
+ const nested: FlattenedEntry[] = [];
311
+
312
+ for (const element of tree) {
313
+ if (element.type === 'rule') {
314
+ const decls = declChildren(element);
315
+ if (decls.length === 0) {
316
+ continue;
317
+ }
318
+ if (mode === 'self' && isSelfSelector(element)) {
319
+ selfSeen = true;
320
+ selfBuilder.appendDecls(decls);
321
+ continue;
322
+ }
323
+ nested.push(buildRule(element, values));
324
+ continue;
325
+ }
326
+ if (element.type.toLowerCase() === '@media') {
327
+ nested.push(buildMedia(element, mode, values));
328
+ continue;
329
+ }
330
+ // decl/comm at top level: only reachable in global mode (self mode wraps
331
+ // everything). Top-level decls are already rejected by the scan.
332
+ }
333
+
334
+ return {
335
+ self:
336
+ mode === 'self' ? (selfSeen ? selfBuilder.build() : emptySlice()) : null,
337
+ nested,
338
+ };
339
+ };
340
+
341
+ const buildRule = (
342
+ element: Element,
343
+ values: readonly unknown[]
344
+ ): FlattenedRule => {
345
+ const builder = new SliceBuilder(values);
346
+ builder.appendDecls(declChildren(element));
347
+ return {
348
+ kind: 'rule',
349
+ alternatives: selectorsOf(element).map((selector) =>
350
+ reconstructSelector(selector, values)
351
+ ),
352
+ slice: builder.build(),
353
+ };
354
+ };
355
+
356
+ const buildMedia = (
357
+ element: Element,
358
+ mode: FlattenMode,
359
+ values: readonly unknown[]
360
+ ): FlattenedMedia => {
361
+ const query = reconstructMediaQuery(selectorsOf(element).join(','), values);
362
+ const children = Array.isArray(element.children) ? element.children : [];
363
+ const selfBuilder = new SliceBuilder(values);
364
+ let selfSeen = false;
365
+ const rules: FlattenedRule[] = [];
366
+
367
+ for (const child of children) {
368
+ if (child.type !== 'rule') {
369
+ if (child.type === 'decl') {
370
+ throw new Error(
371
+ 'Declarations directly inside @media must be within a selector.'
372
+ );
373
+ }
374
+ continue;
375
+ }
376
+ const decls = declChildren(child);
377
+ if (decls.length === 0) {
378
+ continue;
379
+ }
380
+ if (mode === 'self' && isSelfSelector(child)) {
381
+ selfSeen = true;
382
+ selfBuilder.appendDecls(decls);
383
+ continue;
384
+ }
385
+ rules.push(buildRule(child, values));
386
+ }
387
+
388
+ return {
389
+ kind: 'media',
390
+ query,
391
+ ...(selfSeen ? { self: selfBuilder.build() } : {}),
392
+ rules,
393
+ };
394
+ };
395
+
396
+ // Rebuilds a `TemplateSlice` from a block's DECL elements in source order,
397
+ // swapping declaration sentinels back to marker objects and identifier
398
+ // sentinels back to their original values.
399
+ class SliceBuilder {
400
+ private readonly strings: string[] = [''];
401
+ private readonly values: unknown[] = [];
402
+
403
+ constructor(private readonly source: readonly unknown[]) {}
404
+
405
+ appendDecls(decls: readonly Element[]): void {
406
+ for (const decl of decls) {
407
+ const markerMatch = /^--dstl-(\d+)$/.exec(propName(decl));
408
+ if (markerMatch) {
409
+ this.pushValue(this.source[Number(markerMatch[1])]);
410
+ continue;
411
+ }
412
+ this.appendText(typeof decl.value === 'string' ? decl.value : '');
413
+ }
414
+ }
415
+
416
+ private appendText(text: string): void {
417
+ const pattern = /__dstl(\d+)__/g;
418
+ let lastIndex = 0;
419
+ let match: RegExpExecArray | null;
420
+ while ((match = pattern.exec(text)) !== null) {
421
+ this.strings[this.strings.length - 1] += text.slice(
422
+ lastIndex,
423
+ match.index
424
+ );
425
+ const value = this.source[Number(match[1])];
426
+ if (isHandleLike(value)) {
427
+ throw new Error(
428
+ 'A style handle cannot be interpolated into a declaration value; compose with the emotion compat css instead.'
429
+ );
430
+ }
431
+ this.pushValue(value);
432
+ lastIndex = pattern.lastIndex;
433
+ }
434
+ this.strings[this.strings.length - 1] += text.slice(lastIndex);
435
+ }
436
+
437
+ private pushValue(value: unknown): void {
438
+ this.values.push(value);
439
+ this.strings.push('');
440
+ }
441
+
442
+ build(): TemplateSlice {
443
+ return { strings: this.strings, values: this.values };
444
+ }
445
+ }
446
+
447
+ const reconstructSelector = (
448
+ selector: string,
449
+ values: readonly unknown[]
450
+ ): SelectorAlternative => {
451
+ const withSelf = selector.split(SELF_SENTINEL).join('&');
452
+ const parts: SelectorPart[] = [];
453
+ const pattern = /\.?__dstl(\d+)__/g;
454
+ let lastIndex = 0;
455
+ let buffer = '';
456
+ let match: RegExpExecArray | null;
457
+ while ((match = pattern.exec(withSelf)) !== null) {
458
+ buffer += withSelf.slice(lastIndex, match.index);
459
+ if (buffer.length > 0) {
460
+ parts.push(buffer);
461
+ buffer = '';
462
+ }
463
+ const value = values[Number(match[1])];
464
+ if (!isHandleLike(value)) {
465
+ throw new Error(
466
+ `Only style handles may be interpolated into selector position; received ${describe(value)}.`
467
+ );
468
+ }
469
+ parts.push(value);
470
+ lastIndex = pattern.lastIndex;
471
+ }
472
+ buffer += withSelf.slice(lastIndex);
473
+ if (buffer.length > 0 || parts.length === 0) {
474
+ parts.push(buffer);
475
+ }
476
+ return parts;
477
+ };
478
+
479
+ const reconstructMediaQuery = (
480
+ query: string,
481
+ values: readonly unknown[]
482
+ ): string =>
483
+ query.replace(/__dstl(\d+)__/g, (_match, index: string) => {
484
+ const value = values[Number(index)];
485
+ if (typeof value === 'string' || typeof value === 'number') {
486
+ return String(value);
487
+ }
488
+ throw new Error(
489
+ `Media queries may interpolate strings or numbers only; received ${describe(value)}.`
490
+ );
491
+ });
492
+
493
+ // --- Composition inlining ------------------------------------------------
494
+
495
+ const inlineTemplate = (
496
+ strings: readonly string[],
497
+ values: readonly unknown[],
498
+ inlineValue: NonNullable<FlattenOptions['inlineValue']>
499
+ ): TemplateSlice => {
500
+ const outStrings: string[] = [strings[0] ?? ''];
501
+ const outValues: unknown[] = [];
502
+
503
+ const mergeSlice = (slice: TemplateSlice): void => {
504
+ outStrings[outStrings.length - 1] += slice.strings[0] ?? '';
505
+ for (let i = 0; i < slice.values.length; i += 1) {
506
+ outValues.push(slice.values[i]);
507
+ outStrings.push(slice.strings[i + 1] ?? '');
508
+ }
509
+ };
510
+
511
+ values.forEach((value, index) => {
512
+ const position = detectPosition(strings, index);
513
+ const spliced = inlineValue(value, position);
514
+ if (spliced) {
515
+ mergeSlice(inlineTemplate(spliced.strings, spliced.values, inlineValue));
516
+ outStrings[outStrings.length - 1] += strings[index + 1] ?? '';
517
+ return;
518
+ }
519
+ outValues.push(value);
520
+ outStrings.push(strings[index + 1] ?? '');
521
+ });
522
+
523
+ return { strings: outStrings, values: outValues };
524
+ };
525
+
526
+ // Cheap position heuristic: the first structural char in the following string
527
+ // decides. A `{` before any `;`/`}` means the interpolation is building a
528
+ // selector; anything else is a statement/value position.
529
+ const detectPosition = (
530
+ strings: readonly string[],
531
+ index: number
532
+ ): InterpolationPosition => {
533
+ const next = strings[index + 1] ?? '';
534
+ for (const ch of next) {
535
+ if (ch === '{') {
536
+ return 'selector';
537
+ }
538
+ if (ch === ';' || ch === '}') {
539
+ return 'statement';
540
+ }
541
+ }
542
+ return 'statement';
543
+ };
544
+
545
+ // --- Element helpers -----------------------------------------------------
546
+
547
+ const selectorsOf = (element: Element): readonly string[] =>
548
+ Array.isArray(element.props) ? element.props : [element.props];
549
+
550
+ const isSelfSelector = (element: Element): boolean => {
551
+ const selectors = selectorsOf(element);
552
+ return selectors.length === 1 && selectors[0] === SELF_SENTINEL;
553
+ };
554
+
555
+ const declChildren = (element: Element): readonly Element[] =>
556
+ Array.isArray(element.children)
557
+ ? element.children.filter((child) => child.type === 'decl')
558
+ : [];
559
+
560
+ const propName = (element: Element): string =>
561
+ typeof element.props === 'string' ? element.props : (element.props[0] ?? '');
562
+
563
+ // --- Value guards --------------------------------------------------------
564
+
565
+ const isSerializableScalar = (value: unknown): boolean =>
566
+ typeof value === 'string' ||
567
+ typeof value === 'number' ||
568
+ isCssToken(value) ||
569
+ isContextualCssVar(value) ||
570
+ isContextualCssVarName(value) ||
571
+ isScaleToken(value) ||
572
+ isLocalVarRef(value);
573
+
574
+ export const isHandleLike = (value: unknown): value is StyleHandle =>
575
+ Boolean(
576
+ value &&
577
+ typeof value === 'object' &&
578
+ (value as { kind?: string }).kind === 'handle'
579
+ );
580
+
581
+ const emptySlice = (): TemplateSlice => ({ strings: [''], values: [] });
582
+
583
+ const describe = (value: unknown): string => {
584
+ if (value === null) {
585
+ return 'null';
586
+ }
587
+ if (value === undefined) {
588
+ return 'undefined';
589
+ }
590
+ if (typeof value === 'object') {
591
+ const kind = (value as { kind?: string; __kind?: string }).kind;
592
+ const brand = (value as { __kind?: string }).__kind;
593
+ return `object${kind ? ` (kind: ${kind})` : brand ? ` (${brand})` : ''}`;
594
+ }
595
+ return typeof value;
596
+ };