@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.
- package/LICENSE.txt +93 -0
- package/NOTICE.txt +30 -0
- package/README.md +142 -0
- package/THIRD_PARTY_LICENSES.md +768 -0
- package/dist/cjs/collector.js +420 -0
- package/dist/cjs/collector.js.map +1 -0
- package/dist/cjs/css_scan.js +186 -0
- package/dist/cjs/css_scan.js.map +1 -0
- package/dist/cjs/dom_sink.js +40 -0
- package/dist/cjs/dom_sink.js.map +1 -0
- package/dist/cjs/emotion.js +189 -0
- package/dist/cjs/emotion.js.map +1 -0
- package/dist/cjs/engine.js +79 -0
- package/dist/cjs/engine.js.map +1 -0
- package/dist/cjs/environment.js +9 -0
- package/dist/cjs/environment.js.map +1 -0
- package/dist/cjs/hash.js +86 -0
- package/dist/cjs/hash.js.map +1 -0
- package/dist/cjs/idents.js +23 -0
- package/dist/cjs/idents.js.map +1 -0
- package/dist/cjs/index.js +42 -0
- package/dist/cjs/index.js.map +1 -0
- package/dist/cjs/instance.js +34 -0
- package/dist/cjs/instance.js.map +1 -0
- package/dist/cjs/local_vars.js +100 -0
- package/dist/cjs/local_vars.js.map +1 -0
- package/dist/cjs/minify.js +77 -0
- package/dist/cjs/minify.js.map +1 -0
- package/dist/cjs/names.js +77 -0
- package/dist/cjs/names.js.map +1 -0
- package/dist/cjs/nesting.js +406 -0
- package/dist/cjs/nesting.js.map +1 -0
- package/dist/cjs/package.json +3 -0
- package/dist/cjs/runtime.js +257 -0
- package/dist/cjs/runtime.js.map +1 -0
- package/dist/cjs/styles/authoring.js +169 -0
- package/dist/cjs/styles/authoring.js.map +1 -0
- package/dist/cjs/styles/declarations.js +139 -0
- package/dist/cjs/styles/declarations.js.map +1 -0
- package/dist/cjs/styles/index.js +32 -0
- package/dist/cjs/styles/index.js.map +1 -0
- package/dist/cjs/styles/module.js +233 -0
- package/dist/cjs/styles/module.js.map +1 -0
- package/dist/cjs/styles/registry.js +187 -0
- package/dist/cjs/styles/registry.js.map +1 -0
- package/dist/cjs/styles/types.js +9 -0
- package/dist/cjs/styles/types.js.map +1 -0
- package/dist/cjs/testing.js +37 -0
- package/dist/cjs/testing.js.map +1 -0
- package/dist/cjs/theme.js +249 -0
- package/dist/cjs/theme.js.map +1 -0
- package/dist/cjs/tokens/contextual_var.js +39 -0
- package/dist/cjs/tokens/contextual_var.js.map +1 -0
- package/dist/cjs/tokens/index.js +24 -0
- package/dist/cjs/tokens/index.js.map +1 -0
- package/dist/cjs/tokens/kind.js +14 -0
- package/dist/cjs/tokens/kind.js.map +1 -0
- package/dist/cjs/tokens/light_dark.js +42 -0
- package/dist/cjs/tokens/light_dark.js.map +1 -0
- package/dist/cjs/tokens/scale_token.js +28 -0
- package/dist/cjs/tokens/scale_token.js.map +1 -0
- package/dist/cjs/tokens/theme_token.js +28 -0
- package/dist/cjs/tokens/theme_token.js.map +1 -0
- package/dist/cjs/var_invariant.js +115 -0
- package/dist/cjs/var_invariant.js.map +1 -0
- package/dist/collector.d.ts +84 -0
- package/dist/collector.d.ts.map +1 -0
- package/dist/collector.js +416 -0
- package/dist/collector.js.map +1 -0
- package/dist/css_scan.d.ts +57 -0
- package/dist/css_scan.d.ts.map +1 -0
- package/dist/css_scan.js +180 -0
- package/dist/css_scan.js.map +1 -0
- package/dist/dom_sink.d.ts +27 -0
- package/dist/dom_sink.d.ts.map +1 -0
- package/dist/dom_sink.js +36 -0
- package/dist/dom_sink.js.map +1 -0
- package/dist/emotion.d.ts +47 -0
- package/dist/emotion.d.ts.map +1 -0
- package/dist/emotion.js +184 -0
- package/dist/emotion.js.map +1 -0
- package/dist/engine.d.ts +41 -0
- package/dist/engine.d.ts.map +1 -0
- package/dist/engine.js +75 -0
- package/dist/engine.js.map +1 -0
- package/dist/environment.d.ts +43 -0
- package/dist/environment.d.ts.map +1 -0
- package/dist/environment.js +8 -0
- package/dist/environment.js.map +1 -0
- package/dist/hash.d.ts +2 -0
- package/dist/hash.d.ts.map +1 -0
- package/dist/hash.js +82 -0
- package/dist/hash.js.map +1 -0
- package/dist/idents.d.ts +9 -0
- package/dist/idents.d.ts.map +1 -0
- package/dist/idents.js +19 -0
- package/dist/idents.js.map +1 -0
- package/dist/index.d.ts +9 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +14 -0
- package/dist/index.js.map +1 -0
- package/dist/instance.d.ts +16 -0
- package/dist/instance.d.ts.map +1 -0
- package/dist/instance.js +30 -0
- package/dist/instance.js.map +1 -0
- package/dist/local_vars.d.ts +75 -0
- package/dist/local_vars.d.ts.map +1 -0
- package/dist/local_vars.js +92 -0
- package/dist/local_vars.js.map +1 -0
- package/dist/minify.d.ts +11 -0
- package/dist/minify.d.ts.map +1 -0
- package/dist/minify.js +73 -0
- package/dist/minify.js.map +1 -0
- package/dist/names.d.ts +39 -0
- package/dist/names.d.ts.map +1 -0
- package/dist/names.js +70 -0
- package/dist/names.js.map +1 -0
- package/dist/nesting.d.ts +59 -0
- package/dist/nesting.d.ts.map +1 -0
- package/dist/nesting.js +400 -0
- package/dist/nesting.js.map +1 -0
- package/dist/runtime.d.ts +36 -0
- package/dist/runtime.d.ts.map +1 -0
- package/dist/runtime.js +252 -0
- package/dist/runtime.js.map +1 -0
- package/dist/styles/authoring.d.ts +59 -0
- package/dist/styles/authoring.d.ts.map +1 -0
- package/dist/styles/authoring.js +155 -0
- package/dist/styles/authoring.js.map +1 -0
- package/dist/styles/declarations.d.ts +10 -0
- package/dist/styles/declarations.d.ts.map +1 -0
- package/dist/styles/declarations.js +132 -0
- package/dist/styles/declarations.js.map +1 -0
- package/dist/styles/index.d.ts +7 -0
- package/dist/styles/index.d.ts.map +1 -0
- package/dist/styles/index.js +12 -0
- package/dist/styles/index.js.map +1 -0
- package/dist/styles/module.d.ts +5 -0
- package/dist/styles/module.d.ts.map +1 -0
- package/dist/styles/module.js +229 -0
- package/dist/styles/module.js.map +1 -0
- package/dist/styles/registry.d.ts +48 -0
- package/dist/styles/registry.d.ts.map +1 -0
- package/dist/styles/registry.js +182 -0
- package/dist/styles/registry.js.map +1 -0
- package/dist/styles/types.d.ts +211 -0
- package/dist/styles/types.d.ts.map +1 -0
- package/dist/styles/types.js +8 -0
- package/dist/styles/types.js.map +1 -0
- package/dist/testing.d.ts +10 -0
- package/dist/testing.d.ts.map +1 -0
- package/dist/testing.js +31 -0
- package/dist/testing.js.map +1 -0
- package/dist/theme.d.ts +85 -0
- package/dist/theme.d.ts.map +1 -0
- package/dist/theme.js +240 -0
- package/dist/theme.js.map +1 -0
- package/dist/tokens/contextual_var.d.ts +35 -0
- package/dist/tokens/contextual_var.d.ts.map +1 -0
- package/dist/tokens/contextual_var.js +33 -0
- package/dist/tokens/contextual_var.js.map +1 -0
- package/dist/tokens/index.d.ts +5 -0
- package/dist/tokens/index.d.ts.map +1 -0
- package/dist/tokens/index.js +11 -0
- package/dist/tokens/index.js.map +1 -0
- package/dist/tokens/kind.d.ts +2 -0
- package/dist/tokens/kind.d.ts.map +1 -0
- package/dist/tokens/kind.js +10 -0
- package/dist/tokens/kind.js.map +1 -0
- package/dist/tokens/light_dark.d.ts +20 -0
- package/dist/tokens/light_dark.d.ts.map +1 -0
- package/dist/tokens/light_dark.js +37 -0
- package/dist/tokens/light_dark.js.map +1 -0
- package/dist/tokens/scale_token.d.ts +20 -0
- package/dist/tokens/scale_token.d.ts.map +1 -0
- package/dist/tokens/scale_token.js +23 -0
- package/dist/tokens/scale_token.js.map +1 -0
- package/dist/tokens/theme_token.d.ts +20 -0
- package/dist/tokens/theme_token.d.ts.map +1 -0
- package/dist/tokens/theme_token.js +23 -0
- package/dist/tokens/theme_token.js.map +1 -0
- package/dist/var_invariant.d.ts +25 -0
- package/dist/var_invariant.d.ts.map +1 -0
- package/dist/var_invariant.js +108 -0
- package/dist/var_invariant.js.map +1 -0
- package/docs/concepts/authoring.md +115 -0
- package/docs/concepts/collection.md +66 -0
- package/docs/concepts/distillery.md +47 -0
- package/docs/concepts/index.md +18 -0
- package/docs/concepts/naming-and-output.md +123 -0
- package/docs/concepts/ordering.md +61 -0
- package/docs/concepts/single-copy.md +103 -0
- package/docs/concepts/tokens.md +99 -0
- package/docs/docset.yml +53 -0
- package/docs/examples/01-stylesheet.ts +20 -0
- package/docs/examples/02-artifact.ts +26 -0
- package/docs/examples/03-variants.ts +40 -0
- package/docs/examples/04-emotion.ts +33 -0
- package/docs/examples/05-var-invariant.ts +31 -0
- package/docs/examples/06-payload.ts +85 -0
- package/docs/examples/fixture.ts +22 -0
- package/docs/getting-started/index.md +14 -0
- package/docs/getting-started/installation.md +30 -0
- package/docs/getting-started/quick-start.md +61 -0
- package/docs/getting-started/your-first-stylesheet.md +57 -0
- package/docs/guides/compact-artifacts.md +87 -0
- package/docs/guides/emotion-migration.md +60 -0
- package/docs/guides/index.md +17 -0
- package/docs/guides/name-collisions.md +37 -0
- package/docs/guides/non-css-surfaces.md +73 -0
- package/docs/guides/react-renderer.md +158 -0
- package/docs/guides/theming.md +107 -0
- package/docs/guides/var-invariant.md +24 -0
- package/docs/guides/vs-emotion.md +61 -0
- package/docs/index.md +73 -0
- package/docs/logo.svg +7 -0
- package/docs/playground/index.html +12 -0
- package/docs/playground/package.json +23 -0
- package/docs/playground/src/app/app.tsx +109 -0
- package/docs/playground/src/app/css_panel.tsx +119 -0
- package/docs/playground/src/app/editor_panel.tsx +87 -0
- package/docs/playground/src/app/index.ts +8 -0
- package/docs/playground/src/app/monaco_css_editor.tsx +84 -0
- package/docs/playground/src/app/monaco_source_editor.tsx +132 -0
- package/docs/playground/src/app/preview_panel.tsx +99 -0
- package/docs/playground/src/app/tokens_panel.tsx +117 -0
- package/docs/playground/src/app/types.ts +10 -0
- package/docs/playground/src/examples/snippets.test.ts +28 -0
- package/docs/playground/src/examples/snippets.ts +418 -0
- package/docs/playground/src/lib/compile.test.ts +157 -0
- package/docs/playground/src/lib/compile.ts +242 -0
- package/docs/playground/src/lib/demo_environment.test.ts +123 -0
- package/docs/playground/src/lib/demo_environment.ts +140 -0
- package/docs/playground/src/lib/editor_lib.test.ts +39 -0
- package/docs/playground/src/lib/editor_lib.ts +142 -0
- package/docs/playground/src/lib/format_css.test.ts +41 -0
- package/docs/playground/src/lib/format_css.ts +71 -0
- package/docs/playground/src/main.tsx +25 -0
- package/docs/playground/src/setup/chrome.test.ts +47 -0
- package/docs/playground/src/setup/chrome.ts +365 -0
- package/docs/playground/src/setup/monaco_env.ts +64 -0
- package/docs/playground/tsconfig.json +22 -0
- package/docs/playground/vite.config.ts +37 -0
- package/docs/playground/vitest.config.ts +35 -0
- package/docs/playground.md +28 -0
- package/docs/reference/api/emotion.md +372 -0
- package/docs/reference/api/index-1.md +2180 -0
- package/docs/reference/api/index.md +9 -0
- package/docs/reference/api/testing.md +92 -0
- package/docs/reference/api.md +12 -0
- package/docs/reference/entry-points.md +125 -0
- package/docs/reference/environment.md +60 -0
- package/docs/reference/index.md +11 -0
- package/docs/reference/okf-map.md +92 -0
- package/package.json +127 -4
- package/src/collector.ts +526 -0
- package/src/css_scan.ts +263 -0
- package/src/dom_sink.ts +65 -0
- package/src/emotion.ts +311 -0
- package/src/engine.ts +191 -0
- package/src/environment.ts +56 -0
- package/src/hash.ts +111 -0
- package/src/idents.ts +21 -0
- package/src/index.ts +93 -0
- package/src/instance.ts +49 -0
- package/src/local_vars.ts +220 -0
- package/src/minify.ts +87 -0
- package/src/names.ts +129 -0
- package/src/nesting.ts +596 -0
- package/src/runtime.ts +428 -0
- package/src/styles/authoring.ts +244 -0
- package/src/styles/declarations.ts +172 -0
- package/src/styles/index.ts +58 -0
- package/src/styles/module.ts +396 -0
- package/src/styles/registry.ts +261 -0
- package/src/styles/types.ts +283 -0
- package/src/testing.ts +48 -0
- package/src/theme.ts +454 -0
- package/src/tokens/contextual_var.ts +72 -0
- package/src/tokens/index.ts +17 -0
- package/src/tokens/kind.ts +13 -0
- package/src/tokens/light_dark.ts +58 -0
- package/src/tokens/scale_token.ts +41 -0
- package/src/tokens/theme_token.ts +42 -0
- package/src/var_invariant.ts +147 -0
package/src/css_scan.ts
ADDED
|
@@ -0,0 +1,263 @@
|
|
|
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
|
+
// One scanner for the three passes that need to read CSS text: minification,
|
|
9
|
+
// the `@media` canonicalization in `nesting.ts`, and the var invariant in
|
|
10
|
+
// `./testing`. Each of those grew its own character loop, and each learned the
|
|
11
|
+
// same lessons separately and late — comments inside `url(...)`, backslash
|
|
12
|
+
// escapes, at-rules inside `url(...)`, selector-versus-declaration position.
|
|
13
|
+
// A span is the unit all three actually need: a stretch of text with a kind
|
|
14
|
+
// that says whether its contents are code or opaque, plus where it sits.
|
|
15
|
+
//
|
|
16
|
+
// This is deliberately not a CSS Syntax Level 3 tokenizer. It models the six
|
|
17
|
+
// things those passes get wrong and nothing else, because a promise of full
|
|
18
|
+
// conformance is one this module would not keep.
|
|
19
|
+
|
|
20
|
+
/** What a span's text is, which decides whether a pass may look inside it. */
|
|
21
|
+
export type CssSpanKind =
|
|
22
|
+
/** `/* … *\/`, including the delimiters. Opaque. */
|
|
23
|
+
| 'comment'
|
|
24
|
+
/** A quoted string, including the quotes and any escapes. Opaque. */
|
|
25
|
+
| 'string'
|
|
26
|
+
/** An unquoted `url(…)` function token, including the parentheses. Opaque. */
|
|
27
|
+
| 'url'
|
|
28
|
+
/** A backslash and the character it escapes, outside a string. Opaque. */
|
|
29
|
+
| 'escape'
|
|
30
|
+
/** A run of whitespace. */
|
|
31
|
+
| 'whitespace'
|
|
32
|
+
/** One of `{ } : ; , >`. */
|
|
33
|
+
| 'structural'
|
|
34
|
+
/** Anything else, run together: identifiers, values, selectors, `var(`. */
|
|
35
|
+
| 'text';
|
|
36
|
+
|
|
37
|
+
/** Whether the enclosing block holds declarations or further rules. */
|
|
38
|
+
export type CssBlockKind = 'rules' | 'declarations';
|
|
39
|
+
|
|
40
|
+
/** One scanner span: kind, source slice, and block context. */
|
|
41
|
+
export interface CssSpan {
|
|
42
|
+
/** What this span's text is; opaque kinds are skipped by later passes. */
|
|
43
|
+
readonly kind: CssSpanKind;
|
|
44
|
+
/** Source slice for this span, including delimiters. */
|
|
45
|
+
readonly text: string;
|
|
46
|
+
/** Index of the first character in the source. */
|
|
47
|
+
readonly start: number;
|
|
48
|
+
/** Index one past the last character in the source. */
|
|
49
|
+
readonly end: number;
|
|
50
|
+
/**
|
|
51
|
+
* What the enclosing block holds. An at-rule prelude (`@media …`) opens a
|
|
52
|
+
* block of rules; anything else opens declarations; top level is rules.
|
|
53
|
+
*
|
|
54
|
+
* This is what separates `color : red` — where the space before `:` is
|
|
55
|
+
* noise — from `.menu :hover`, where it is a descendant combinator.
|
|
56
|
+
*/
|
|
57
|
+
readonly block: CssBlockKind;
|
|
58
|
+
/** Block nesting depth; `0` at top level. */
|
|
59
|
+
readonly depth: number;
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
const STRUCTURAL = new Set(['{', '}', ':', ';', ',', '>']);
|
|
63
|
+
const URL_PREFIX = 'url(';
|
|
64
|
+
|
|
65
|
+
/** `true` when an unquoted `url(` function token starts at `index`. */
|
|
66
|
+
const isUnquotedUrlStart = (value: string, index: number): boolean => {
|
|
67
|
+
// Cheap rejection first: this runs at every index of every text span, and
|
|
68
|
+
// all but one character in a few hundred fails here rather than in a
|
|
69
|
+
// `slice().toLowerCase()`.
|
|
70
|
+
const head = value[index];
|
|
71
|
+
if (head !== 'u' && head !== 'U') {
|
|
72
|
+
return false;
|
|
73
|
+
}
|
|
74
|
+
// `url(` has to open a function token rather than end a longer identifier.
|
|
75
|
+
// `myurl(/a/*b*/c)` is some other function, and reading it as a url would
|
|
76
|
+
// make its contents opaque — hiding a real comment from every pass.
|
|
77
|
+
const before = value[index - 1];
|
|
78
|
+
if (before !== undefined && /[\w\\-]/.test(before)) {
|
|
79
|
+
return false;
|
|
80
|
+
}
|
|
81
|
+
if (
|
|
82
|
+
value.slice(index, index + URL_PREFIX.length).toLowerCase() !== URL_PREFIX
|
|
83
|
+
) {
|
|
84
|
+
return false;
|
|
85
|
+
}
|
|
86
|
+
let cursor = index + URL_PREFIX.length;
|
|
87
|
+
while (cursor < value.length && /\s/.test(value[cursor] ?? '')) {
|
|
88
|
+
cursor += 1;
|
|
89
|
+
}
|
|
90
|
+
const first = value[cursor];
|
|
91
|
+
return first !== '"' && first !== "'";
|
|
92
|
+
};
|
|
93
|
+
|
|
94
|
+
/** Index one past an unquoted `url(…)` token that starts at `start`. */
|
|
95
|
+
const endOfUrl = (value: string, start: number): number => {
|
|
96
|
+
for (
|
|
97
|
+
let index = start + URL_PREFIX.length;
|
|
98
|
+
index < value.length;
|
|
99
|
+
index += 1
|
|
100
|
+
) {
|
|
101
|
+
const char = value[index];
|
|
102
|
+
if (char === '\\' && index + 1 < value.length) {
|
|
103
|
+
index += 1;
|
|
104
|
+
continue;
|
|
105
|
+
}
|
|
106
|
+
if (char === ')') {
|
|
107
|
+
return index + 1;
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
return value.length;
|
|
111
|
+
};
|
|
112
|
+
|
|
113
|
+
/** Index one past a quoted string that starts at `start`. Unterminated runs to the end. */
|
|
114
|
+
const endOfString = (value: string, start: number): number => {
|
|
115
|
+
const quote = value[start];
|
|
116
|
+
for (let index = start + 1; index < value.length; index += 1) {
|
|
117
|
+
const char = value[index];
|
|
118
|
+
if (char === '\\' && index + 1 < value.length) {
|
|
119
|
+
index += 1;
|
|
120
|
+
continue;
|
|
121
|
+
}
|
|
122
|
+
if (char === quote) {
|
|
123
|
+
return index + 1;
|
|
124
|
+
}
|
|
125
|
+
}
|
|
126
|
+
return value.length;
|
|
127
|
+
};
|
|
128
|
+
|
|
129
|
+
/** Index one past a `/* … *\/` comment that starts at `start`. Unterminated runs to the end. */
|
|
130
|
+
const endOfComment = (value: string, start: number): number => {
|
|
131
|
+
const closing = value.indexOf('*/', start + 2);
|
|
132
|
+
return closing === -1 ? value.length : closing + 2;
|
|
133
|
+
};
|
|
134
|
+
|
|
135
|
+
/**
|
|
136
|
+
* Splits `value` into spans.
|
|
137
|
+
*
|
|
138
|
+
* Concatenating every span's `text` reproduces `value` exactly, which is what
|
|
139
|
+
* lets a pass rewrite the spans it understands and copy the rest through.
|
|
140
|
+
*
|
|
141
|
+
* @param value CSS text.
|
|
142
|
+
*/
|
|
143
|
+
export const scanCss = (value: string): readonly CssSpan[] => {
|
|
144
|
+
const spans: CssSpan[] = [];
|
|
145
|
+
const blocks: CssBlockKind[] = ['rules'];
|
|
146
|
+
// Null until the first significant character of a prelude is seen, so that
|
|
147
|
+
// `@media …{` opens rules and `.a …{` opens declarations.
|
|
148
|
+
let preludeIsAtRule: boolean | null = null;
|
|
149
|
+
let index = 0;
|
|
150
|
+
|
|
151
|
+
const push = (kind: CssSpanKind, start: number, end: number): void => {
|
|
152
|
+
spans.push({
|
|
153
|
+
kind,
|
|
154
|
+
text: value.slice(start, end),
|
|
155
|
+
start,
|
|
156
|
+
end,
|
|
157
|
+
block: blocks.at(-1) ?? 'rules',
|
|
158
|
+
depth: blocks.length - 1,
|
|
159
|
+
});
|
|
160
|
+
};
|
|
161
|
+
|
|
162
|
+
while (index < value.length) {
|
|
163
|
+
const char = value[index] ?? '';
|
|
164
|
+
const next = value[index + 1];
|
|
165
|
+
|
|
166
|
+
if (char === '/' && next === '*') {
|
|
167
|
+
const end = endOfComment(value, index);
|
|
168
|
+
push('comment', index, end);
|
|
169
|
+
index = end;
|
|
170
|
+
continue;
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
if (char === '"' || char === "'") {
|
|
174
|
+
const end = endOfString(value, index);
|
|
175
|
+
push('string', index, end);
|
|
176
|
+
index = end;
|
|
177
|
+
continue;
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
if (char === '\\' && next !== undefined) {
|
|
181
|
+
push('escape', index, index + 2);
|
|
182
|
+
index += 2;
|
|
183
|
+
continue;
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
if (isUnquotedUrlStart(value, index)) {
|
|
187
|
+
const end = endOfUrl(value, index);
|
|
188
|
+
push('url', index, end);
|
|
189
|
+
index = end;
|
|
190
|
+
continue;
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
if (/\s/.test(char)) {
|
|
194
|
+
let end = index + 1;
|
|
195
|
+
while (end < value.length && /\s/.test(value[end] ?? '')) {
|
|
196
|
+
end += 1;
|
|
197
|
+
}
|
|
198
|
+
push('whitespace', index, end);
|
|
199
|
+
index = end;
|
|
200
|
+
continue;
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
if (STRUCTURAL.has(char)) {
|
|
204
|
+
// Depth changes after the span is recorded, so `{` reads as belonging to
|
|
205
|
+
// the block it closes over rather than the one it opens.
|
|
206
|
+
push('structural', index, index + 1);
|
|
207
|
+
if (char === '{') {
|
|
208
|
+
blocks.push(preludeIsAtRule ? 'rules' : 'declarations');
|
|
209
|
+
} else if (char === '}' && blocks.length > 1) {
|
|
210
|
+
blocks.pop();
|
|
211
|
+
}
|
|
212
|
+
if (char === '{' || char === '}' || char === ';') {
|
|
213
|
+
preludeIsAtRule = null;
|
|
214
|
+
}
|
|
215
|
+
index += 1;
|
|
216
|
+
continue;
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
if (preludeIsAtRule === null) {
|
|
220
|
+
preludeIsAtRule = char === '@';
|
|
221
|
+
}
|
|
222
|
+
let end = index + 1;
|
|
223
|
+
while (end < value.length) {
|
|
224
|
+
const ahead = value[end] ?? '';
|
|
225
|
+
if (
|
|
226
|
+
STRUCTURAL.has(ahead) ||
|
|
227
|
+
/\s/.test(ahead) ||
|
|
228
|
+
ahead === '"' ||
|
|
229
|
+
ahead === "'" ||
|
|
230
|
+
ahead === '\\' ||
|
|
231
|
+
(ahead === '/' && value[end + 1] === '*') ||
|
|
232
|
+
isUnquotedUrlStart(value, end)
|
|
233
|
+
) {
|
|
234
|
+
break;
|
|
235
|
+
}
|
|
236
|
+
end += 1;
|
|
237
|
+
}
|
|
238
|
+
push('text', index, end);
|
|
239
|
+
index = end;
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
return spans;
|
|
243
|
+
};
|
|
244
|
+
|
|
245
|
+
/**
|
|
246
|
+
* `true` for spans whose text is a literal rather than code — a comment, a
|
|
247
|
+
* quoted string, an unquoted `url(...)`, or an escape pair. A pass may rewrite
|
|
248
|
+
* what is inside a code span and must copy an opaque one through untouched.
|
|
249
|
+
*/
|
|
250
|
+
export const isOpaque = (span: CssSpan): boolean =>
|
|
251
|
+
span.kind === 'comment' ||
|
|
252
|
+
span.kind === 'string' ||
|
|
253
|
+
span.kind === 'url' ||
|
|
254
|
+
span.kind === 'escape';
|
|
255
|
+
|
|
256
|
+
/** True when `value` has no declarations left after comments, whitespace, and bare `;` are ignored. */
|
|
257
|
+
export const isBlankCss = (value: string): boolean =>
|
|
258
|
+
scanCss(value).every(
|
|
259
|
+
(span) =>
|
|
260
|
+
span.kind === 'comment' ||
|
|
261
|
+
span.kind === 'whitespace' ||
|
|
262
|
+
(span.kind === 'structural' && span.text === ';')
|
|
263
|
+
);
|
package/src/dom_sink.ts
ADDED
|
@@ -0,0 +1,65 @@
|
|
|
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
|
+
import type { StyleSink } from './emotion';
|
|
9
|
+
|
|
10
|
+
/** Minimal interface for a DOM `<style>` element. */
|
|
11
|
+
export interface StyleElementLike {
|
|
12
|
+
/** Current stylesheet text, or `null` before the first flush. */
|
|
13
|
+
textContent: string | null;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
/** Minimal interface for a DOM `Document` able to create and append style tags. */
|
|
17
|
+
export interface DocumentLike {
|
|
18
|
+
/** Creates a `<style>` element. */
|
|
19
|
+
createElement(tagName: 'style'): StyleElementLike;
|
|
20
|
+
/** Node that receives the created style element. */
|
|
21
|
+
readonly head: { appendChild(node: StyleElementLike): void };
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
/** Host document and optional flush scheduler for a `<style>` sink. */
|
|
25
|
+
export interface CreateDomSinkOptions {
|
|
26
|
+
/** The host document where the style element will be appended. */
|
|
27
|
+
document: DocumentLike;
|
|
28
|
+
/** Defers the flush. Defaults to `queueMicrotask`. */
|
|
29
|
+
schedule?: (flush: () => void) => void;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
/**
|
|
33
|
+
* One managed `<style>` element, rewritten on each flush.
|
|
34
|
+
*/
|
|
35
|
+
export const createDomSink = ({
|
|
36
|
+
document,
|
|
37
|
+
schedule = queueMicrotask,
|
|
38
|
+
}: CreateDomSinkOptions): StyleSink => {
|
|
39
|
+
let element: StyleElementLike | null = null;
|
|
40
|
+
let dirty = false;
|
|
41
|
+
let latestRender: (() => string) | null = null;
|
|
42
|
+
|
|
43
|
+
const flush = (): void => {
|
|
44
|
+
dirty = false;
|
|
45
|
+
if (!latestRender) {
|
|
46
|
+
return;
|
|
47
|
+
}
|
|
48
|
+
if (!element) {
|
|
49
|
+
element = document.createElement('style');
|
|
50
|
+
document.head.appendChild(element);
|
|
51
|
+
}
|
|
52
|
+
element.textContent = latestRender();
|
|
53
|
+
};
|
|
54
|
+
|
|
55
|
+
return {
|
|
56
|
+
invalidate(render) {
|
|
57
|
+
latestRender = render;
|
|
58
|
+
if (dirty) {
|
|
59
|
+
return;
|
|
60
|
+
}
|
|
61
|
+
dirty = true;
|
|
62
|
+
schedule(flush);
|
|
63
|
+
},
|
|
64
|
+
};
|
|
65
|
+
};
|
package/src/emotion.ts
ADDED
|
@@ -0,0 +1,311 @@
|
|
|
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
|
+
import type { Distillery } from './engine';
|
|
9
|
+
import { contentHash64 } from './hash';
|
|
10
|
+
import { flattenTemplate, isHandleLike, type TemplateSlice } from './nesting';
|
|
11
|
+
import {
|
|
12
|
+
globalStylesFromFlattened,
|
|
13
|
+
pendingHandleFromFlattened,
|
|
14
|
+
type StyleHandle,
|
|
15
|
+
type StyleRegistry,
|
|
16
|
+
type StylesModule,
|
|
17
|
+
} from './styles';
|
|
18
|
+
|
|
19
|
+
export {
|
|
20
|
+
createDomSink,
|
|
21
|
+
type CreateDomSinkOptions,
|
|
22
|
+
type DocumentLike,
|
|
23
|
+
type StyleElementLike,
|
|
24
|
+
} from './dom_sink';
|
|
25
|
+
|
|
26
|
+
/** Receives stylesheet invalidation from Emotion compat modules. */
|
|
27
|
+
export interface StyleSink {
|
|
28
|
+
/** Marks the sink dirty. `render` produces the current stylesheet when it flushes. */
|
|
29
|
+
invalidate(render: () => string): void;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
/** Shared sink for Emotion compat modules on one registry. */
|
|
33
|
+
export interface CreateEmotionOptions {
|
|
34
|
+
/** Notified when a compat module registers. Shared across `createEmotion` calls on the same registry. */
|
|
35
|
+
sink?: StyleSink;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
/** Style handle that stringifies to its readable class name. */
|
|
39
|
+
export interface EmotionCss extends StyleHandle {
|
|
40
|
+
/** Stringifies to the readable class name. */
|
|
41
|
+
toString(): string;
|
|
42
|
+
/** Template interpolation; same as {@link EmotionCss.toString}. */
|
|
43
|
+
[Symbol.toPrimitive](hint: string): string;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
/** One `cx(...)` argument: a class, handle, list, or truthy map. */
|
|
47
|
+
export type ClassNameArg =
|
|
48
|
+
| string
|
|
49
|
+
| number
|
|
50
|
+
| false
|
|
51
|
+
| null
|
|
52
|
+
| undefined
|
|
53
|
+
| EmotionCss
|
|
54
|
+
| StyleHandle
|
|
55
|
+
| ClassNameArg[]
|
|
56
|
+
| { readonly [className: string]: boolean };
|
|
57
|
+
|
|
58
|
+
/** Emotion-shaped authoring API bound to one Distillery registry. */
|
|
59
|
+
export interface Emotion {
|
|
60
|
+
/** Authors styles and returns a handle that can be used directly or passed to `cx`. */
|
|
61
|
+
css(strings: TemplateStringsArray, ...values: readonly unknown[]): EmotionCss;
|
|
62
|
+
/** Combines handles, conditional classes, and plain strings into a space-separated string. */
|
|
63
|
+
cx(...args: readonly ClassNameArg[]): string;
|
|
64
|
+
/** Defines styles that apply globally (e.g. `body`, `html`). */
|
|
65
|
+
injectGlobal(
|
|
66
|
+
strings: TemplateStringsArray,
|
|
67
|
+
...values: readonly unknown[]
|
|
68
|
+
): void;
|
|
69
|
+
/** Emits the full CSS for all generated emotion styles. */
|
|
70
|
+
stylesheet(): string;
|
|
71
|
+
/** Returns the underlying modules backing `injectGlobal` calls. */
|
|
72
|
+
globalModules(): readonly StylesModule[];
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
/** Per-registry Emotion compat cache. */
|
|
76
|
+
interface EmotionState {
|
|
77
|
+
/** `css` handles keyed by source hash. */
|
|
78
|
+
readonly wrappers: Map<string, EmotionCss>;
|
|
79
|
+
/** Original templates for inlining nested compat handles. */
|
|
80
|
+
readonly rawSources: Map<string, TemplateSlice>;
|
|
81
|
+
/** Module name → canonical source hash. */
|
|
82
|
+
readonly canonicalByModule: Map<string, string>;
|
|
83
|
+
/** Modules produced by `injectGlobal`. */
|
|
84
|
+
readonly globals: StylesModule[];
|
|
85
|
+
/** Handles created by this registry's Emotion instance. */
|
|
86
|
+
readonly brand: WeakSet<object>;
|
|
87
|
+
/** Sinks notified when a compat module registers. */
|
|
88
|
+
readonly sinks: Set<StyleSink>;
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
const stateByRegistry = new WeakMap<StyleRegistry, EmotionState>();
|
|
92
|
+
|
|
93
|
+
const stateFor = (registry: StyleRegistry): EmotionState => {
|
|
94
|
+
let state = stateByRegistry.get(registry);
|
|
95
|
+
if (!state) {
|
|
96
|
+
state = {
|
|
97
|
+
wrappers: new Map(),
|
|
98
|
+
rawSources: new Map(),
|
|
99
|
+
canonicalByModule: new Map(),
|
|
100
|
+
globals: [],
|
|
101
|
+
brand: new WeakSet(),
|
|
102
|
+
sinks: new Set(),
|
|
103
|
+
};
|
|
104
|
+
stateByRegistry.set(registry, state);
|
|
105
|
+
}
|
|
106
|
+
return state;
|
|
107
|
+
};
|
|
108
|
+
|
|
109
|
+
/**
|
|
110
|
+
* `@emotion/css`-shaped API over a distillery.
|
|
111
|
+
*
|
|
112
|
+
* `String(css\`...\`)` is the readable class name and does not collect. Passing the wrapper through `resolveClassName` collects and compacts like a native handle.
|
|
113
|
+
*
|
|
114
|
+
* @param distillery Bound distillery that owns the underlying registry.
|
|
115
|
+
* @param options Optional sink for DOM injection.
|
|
116
|
+
*/
|
|
117
|
+
export const createEmotion = <TTokens>(
|
|
118
|
+
distillery: Distillery<TTokens>,
|
|
119
|
+
options: CreateEmotionOptions = {}
|
|
120
|
+
): Emotion => {
|
|
121
|
+
const { registry } = distillery;
|
|
122
|
+
const state = stateFor(registry);
|
|
123
|
+
|
|
124
|
+
const stylesheet = (): string =>
|
|
125
|
+
distillery.renderStyles(distillery.stylesheetCollector('readable'));
|
|
126
|
+
|
|
127
|
+
const invalidateSinks = (): void => {
|
|
128
|
+
for (const sink of state.sinks) {
|
|
129
|
+
sink.invalidate(stylesheet);
|
|
130
|
+
}
|
|
131
|
+
};
|
|
132
|
+
|
|
133
|
+
if (options.sink) {
|
|
134
|
+
state.sinks.add(options.sink);
|
|
135
|
+
// Catch a late-attached sink up to styles already registered on this
|
|
136
|
+
// registry: cached `css`/`injectGlobal` calls return early without
|
|
137
|
+
// invalidating, so without this the managed `<style>` would miss every
|
|
138
|
+
// style registered before the sink was attached.
|
|
139
|
+
options.sink.invalidate(stylesheet);
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
const inlineValue = (
|
|
143
|
+
value: unknown,
|
|
144
|
+
position: 'selector' | 'statement'
|
|
145
|
+
): TemplateSlice | null => {
|
|
146
|
+
if (position !== 'statement' || !isBranded(state, value)) {
|
|
147
|
+
return null;
|
|
148
|
+
}
|
|
149
|
+
return state.rawSources.get((value as StyleHandle).key) ?? null;
|
|
150
|
+
};
|
|
151
|
+
|
|
152
|
+
const css = (
|
|
153
|
+
strings: TemplateStringsArray,
|
|
154
|
+
...values: readonly unknown[]
|
|
155
|
+
): EmotionCss => {
|
|
156
|
+
assertTemplate(strings, 'css');
|
|
157
|
+
const hash = contentHash64(strings, values);
|
|
158
|
+
const cached = state.wrappers.get(hash);
|
|
159
|
+
if (cached) {
|
|
160
|
+
return cached;
|
|
161
|
+
}
|
|
162
|
+
const moduleName = `css-${hash}`;
|
|
163
|
+
const canonical = canonicalKey(strings, values);
|
|
164
|
+
detectCollision(state, moduleName, canonical);
|
|
165
|
+
|
|
166
|
+
const flat = flattenTemplate(strings, values, {
|
|
167
|
+
mode: 'self',
|
|
168
|
+
inlineValue,
|
|
169
|
+
});
|
|
170
|
+
const pending = pendingHandleFromFlattened(flat);
|
|
171
|
+
const module = distillery.createStyleModule(moduleName, () => ({
|
|
172
|
+
root: pending,
|
|
173
|
+
}));
|
|
174
|
+
const handle = module.handles.root;
|
|
175
|
+
|
|
176
|
+
state.rawSources.set(handle.key, {
|
|
177
|
+
strings: [...strings],
|
|
178
|
+
values: [...values],
|
|
179
|
+
});
|
|
180
|
+
const wrapper = makeWrapper(handle);
|
|
181
|
+
state.brand.add(wrapper);
|
|
182
|
+
state.wrappers.set(hash, wrapper);
|
|
183
|
+
state.canonicalByModule.set(moduleName, canonical);
|
|
184
|
+
invalidateSinks();
|
|
185
|
+
return wrapper;
|
|
186
|
+
};
|
|
187
|
+
|
|
188
|
+
const injectGlobal = (
|
|
189
|
+
strings: TemplateStringsArray,
|
|
190
|
+
...values: readonly unknown[]
|
|
191
|
+
): void => {
|
|
192
|
+
assertTemplate(strings, 'injectGlobal');
|
|
193
|
+
const hash = contentHash64(strings, values);
|
|
194
|
+
const moduleName = `global-${hash}`;
|
|
195
|
+
if (registry.module(moduleName)) {
|
|
196
|
+
return;
|
|
197
|
+
}
|
|
198
|
+
const canonical = canonicalKey(strings, values);
|
|
199
|
+
detectCollision(state, moduleName, canonical);
|
|
200
|
+
|
|
201
|
+
const flat = flattenTemplate(strings, values, {
|
|
202
|
+
mode: 'global',
|
|
203
|
+
inlineValue,
|
|
204
|
+
});
|
|
205
|
+
const module = distillery.createStyleModule(moduleName, () =>
|
|
206
|
+
globalStylesFromFlattened(flat)
|
|
207
|
+
);
|
|
208
|
+
state.canonicalByModule.set(moduleName, canonical);
|
|
209
|
+
state.globals.push(module);
|
|
210
|
+
invalidateSinks();
|
|
211
|
+
};
|
|
212
|
+
|
|
213
|
+
const cx = (...args: readonly ClassNameArg[]): string => {
|
|
214
|
+
const classes: string[] = [];
|
|
215
|
+
const walk = (arg: ClassNameArg): void => {
|
|
216
|
+
if (!arg) {
|
|
217
|
+
return;
|
|
218
|
+
}
|
|
219
|
+
if (typeof arg === 'string') {
|
|
220
|
+
classes.push(arg);
|
|
221
|
+
return;
|
|
222
|
+
}
|
|
223
|
+
if (typeof arg === 'number') {
|
|
224
|
+
classes.push(String(arg));
|
|
225
|
+
return;
|
|
226
|
+
}
|
|
227
|
+
if (Array.isArray(arg)) {
|
|
228
|
+
arg.forEach(walk);
|
|
229
|
+
return;
|
|
230
|
+
}
|
|
231
|
+
if (isBranded(state, arg)) {
|
|
232
|
+
classes.push(String(arg));
|
|
233
|
+
return;
|
|
234
|
+
}
|
|
235
|
+
if (isHandleLike(arg)) {
|
|
236
|
+
classes.push(arg.readableName);
|
|
237
|
+
return;
|
|
238
|
+
}
|
|
239
|
+
for (const [className, enabled] of Object.entries(arg)) {
|
|
240
|
+
if (enabled) {
|
|
241
|
+
classes.push(className);
|
|
242
|
+
}
|
|
243
|
+
}
|
|
244
|
+
};
|
|
245
|
+
args.forEach(walk);
|
|
246
|
+
return classes.join(' ');
|
|
247
|
+
};
|
|
248
|
+
|
|
249
|
+
return {
|
|
250
|
+
css,
|
|
251
|
+
cx,
|
|
252
|
+
injectGlobal,
|
|
253
|
+
stylesheet,
|
|
254
|
+
globalModules: () => [...state.globals],
|
|
255
|
+
};
|
|
256
|
+
};
|
|
257
|
+
|
|
258
|
+
const makeWrapper = (handle: StyleHandle): EmotionCss => {
|
|
259
|
+
const readable = handle.readableName;
|
|
260
|
+
const asString = (): string => readable;
|
|
261
|
+
// Spread copy keeps `resolveClassName` (closed over the original handle) and
|
|
262
|
+
// `key` intact so collector dedupe and compaction work unchanged.
|
|
263
|
+
const wrapper = { ...handle } as EmotionCss & {
|
|
264
|
+
toString: () => string;
|
|
265
|
+
[Symbol.toPrimitive]: () => string;
|
|
266
|
+
};
|
|
267
|
+
wrapper.toString = asString;
|
|
268
|
+
wrapper[Symbol.toPrimitive] = asString;
|
|
269
|
+
return wrapper;
|
|
270
|
+
};
|
|
271
|
+
|
|
272
|
+
const isBranded = (state: EmotionState, value: unknown): value is EmotionCss =>
|
|
273
|
+
Boolean(value && typeof value === 'object' && state.brand.has(value));
|
|
274
|
+
|
|
275
|
+
const assertTemplate = (strings: unknown, fn: string): void => {
|
|
276
|
+
if (!Array.isArray(strings) || !('raw' in (strings as object))) {
|
|
277
|
+
throw new Error(
|
|
278
|
+
`${fn} is a template tag; object styles (${fn}({...})) are not supported.`
|
|
279
|
+
);
|
|
280
|
+
}
|
|
281
|
+
};
|
|
282
|
+
|
|
283
|
+
const detectCollision = (
|
|
284
|
+
state: EmotionState,
|
|
285
|
+
moduleName: string,
|
|
286
|
+
canonical: string
|
|
287
|
+
): void => {
|
|
288
|
+
const prior = state.canonicalByModule.get(moduleName);
|
|
289
|
+
if (prior !== undefined && prior !== canonical) {
|
|
290
|
+
throw new Error(
|
|
291
|
+
`Content-hash collision for "${moduleName}"; two distinct templates hashed to the same name.`
|
|
292
|
+
);
|
|
293
|
+
}
|
|
294
|
+
};
|
|
295
|
+
|
|
296
|
+
// A cheap, exact collision tripwire independent of the (lossy) hash. NUL-joins
|
|
297
|
+
// strings and String()s values — good enough because it is only ever compared
|
|
298
|
+
// against another canonical string for the same module name.
|
|
299
|
+
const canonicalKey = (
|
|
300
|
+
strings: readonly string[],
|
|
301
|
+
values: readonly unknown[]
|
|
302
|
+
): string => {
|
|
303
|
+
const segments: string[] = [];
|
|
304
|
+
strings.forEach((part, index) => {
|
|
305
|
+
segments.push(part);
|
|
306
|
+
if (index < values.length) {
|
|
307
|
+
segments.push(String(values[index]));
|
|
308
|
+
}
|
|
309
|
+
});
|
|
310
|
+
return segments.join('\u0000');
|
|
311
|
+
};
|