@hajimism/dek 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 (187) hide show
  1. package/LICENSE +21 -0
  2. package/README.ja.md +75 -0
  3. package/README.md +75 -0
  4. package/package.json +65 -0
  5. package/src/cli/address.ts +53 -0
  6. package/src/cli/build.ts +85 -0
  7. package/src/cli/check.ts +97 -0
  8. package/src/cli/commands.ts +851 -0
  9. package/src/cli/contract.ts +444 -0
  10. package/src/cli/cues.ts +18 -0
  11. package/src/cli/files.ts +309 -0
  12. package/src/cli/flags.ts +295 -0
  13. package/src/cli/format.ts +156 -0
  14. package/src/cli/goto.ts +96 -0
  15. package/src/cli/init.ts +136 -0
  16. package/src/cli/keep-alive.ts +72 -0
  17. package/src/cli/lint.ts +84 -0
  18. package/src/cli/ls.ts +119 -0
  19. package/src/cli/main.ts +231 -0
  20. package/src/cli/marks.ts +16 -0
  21. package/src/cli/mv.ts +37 -0
  22. package/src/cli/new.ts +64 -0
  23. package/src/cli/pdf.ts +16 -0
  24. package/src/cli/qr.ts +466 -0
  25. package/src/cli/ref.ts +165 -0
  26. package/src/cli/rehearse.ts +42 -0
  27. package/src/cli/result.ts +161 -0
  28. package/src/cli/scope.ts +308 -0
  29. package/src/cli/serve.ts +125 -0
  30. package/src/cli/shot.ts +134 -0
  31. package/src/cli/show.ts +103 -0
  32. package/src/cli/sync.ts +37 -0
  33. package/src/cli/text.ts +479 -0
  34. package/src/cli/theme.ts +56 -0
  35. package/src/cli/tty.ts +81 -0
  36. package/src/cli/usage.ts +198 -0
  37. package/src/cli/video.ts +17 -0
  38. package/src/cli/voice.ts +113 -0
  39. package/src/cli.ts +4 -0
  40. package/src/core/assets.ts +239 -0
  41. package/src/core/build.ts +77 -0
  42. package/src/core/bundled-theme.ts +15 -0
  43. package/src/core/capture-go.ts +72 -0
  44. package/src/core/chrome.ts +128 -0
  45. package/src/core/config-keys.ts +82 -0
  46. package/src/core/config.ts +95 -0
  47. package/src/core/css-scan.ts +158 -0
  48. package/src/core/css-transform.ts +140 -0
  49. package/src/core/css-value.ts +127 -0
  50. package/src/core/css.ts +545 -0
  51. package/src/core/cue.ts +187 -0
  52. package/src/core/deck-paths.ts +34 -0
  53. package/src/core/diagnostic.ts +114 -0
  54. package/src/core/document.ts +218 -0
  55. package/src/core/error.ts +31 -0
  56. package/src/core/escape.ts +16 -0
  57. package/src/core/fill.ts +100 -0
  58. package/src/core/finish-beat.ts +30 -0
  59. package/src/core/github.ts +234 -0
  60. package/src/core/html-scan.ts +253 -0
  61. package/src/core/html.ts +234 -0
  62. package/src/core/in-page-go.ts +128 -0
  63. package/src/core/lines.ts +43 -0
  64. package/src/core/lint/asset-refs.ts +109 -0
  65. package/src/core/lint/context.ts +116 -0
  66. package/src/core/lint/files.ts +117 -0
  67. package/src/core/lint/keys.ts +48 -0
  68. package/src/core/lint/morph.ts +91 -0
  69. package/src/core/lint/script.ts +102 -0
  70. package/src/core/lint/shared-styles.ts +165 -0
  71. package/src/core/lint/slide-html.ts +466 -0
  72. package/src/core/lint/slide-script.ts +179 -0
  73. package/src/core/lint/theme-css.ts +295 -0
  74. package/src/core/lint/timing.ts +66 -0
  75. package/src/core/lint/tokens.ts +520 -0
  76. package/src/core/lint/voice.ts +76 -0
  77. package/src/core/lint.ts +137 -0
  78. package/src/core/live-protocol.ts +130 -0
  79. package/src/core/marks.ts +198 -0
  80. package/src/core/mv.ts +293 -0
  81. package/src/core/ogp.ts +99 -0
  82. package/src/core/optional.ts +24 -0
  83. package/src/core/overflow.ts +220 -0
  84. package/src/core/page-pool.ts +53 -0
  85. package/src/core/page.ts +66 -0
  86. package/src/core/parse.ts +307 -0
  87. package/src/core/path.ts +58 -0
  88. package/src/core/pdf.ts +71 -0
  89. package/src/core/playwright-module.d.ts +43 -0
  90. package/src/core/playwright-motion.ts +42 -0
  91. package/src/core/playwright-sheet.ts +32 -0
  92. package/src/core/playwright-visual.ts +391 -0
  93. package/src/core/playwright-worker.ts +5 -0
  94. package/src/core/playwright.ts +431 -0
  95. package/src/core/position.ts +146 -0
  96. package/src/core/presenter-state.ts +43 -0
  97. package/src/core/presenter.ts +131 -0
  98. package/src/core/prose.ts +16 -0
  99. package/src/core/pseudo-text.ts +67 -0
  100. package/src/core/rail-width.ts +4 -0
  101. package/src/core/ref-name.ts +110 -0
  102. package/src/core/ref.ts +238 -0
  103. package/src/core/resolve.ts +228 -0
  104. package/src/core/rumdl.ts +99 -0
  105. package/src/core/safe-fs.ts +196 -0
  106. package/src/core/sarif.ts +135 -0
  107. package/src/core/schema.ts +89 -0
  108. package/src/core/script-lines.ts +53 -0
  109. package/src/core/sheet.ts +223 -0
  110. package/src/core/shot/morph.ts +80 -0
  111. package/src/core/shot/motion.ts +187 -0
  112. package/src/core/shot/still.ts +216 -0
  113. package/src/core/shot-cache.ts +140 -0
  114. package/src/core/size.ts +11 -0
  115. package/src/core/skeleton-record.ts +51 -0
  116. package/src/core/skeleton.ts +89 -0
  117. package/src/core/slide-draw.ts +165 -0
  118. package/src/core/slide-eval-worker.ts +58 -0
  119. package/src/core/slide-measure.ts +272 -0
  120. package/src/core/slide-script-eval.ts +258 -0
  121. package/src/core/slide-script.ts +252 -0
  122. package/src/core/spawn.ts +128 -0
  123. package/src/core/step.ts +94 -0
  124. package/src/core/still-page.ts +70 -0
  125. package/src/core/suggest.ts +33 -0
  126. package/src/core/sync.ts +316 -0
  127. package/src/core/text-contrast.ts +466 -0
  128. package/src/core/theme-excerpt.ts +187 -0
  129. package/src/core/theme-facts.ts +51 -0
  130. package/src/core/timeline.ts +216 -0
  131. package/src/core/timing.ts +88 -0
  132. package/src/core/toml-keys.ts +349 -0
  133. package/src/core/url-attributes.ts +103 -0
  134. package/src/core/video-document.ts +15 -0
  135. package/src/core/visual-cache.ts +100 -0
  136. package/src/core/visual.ts +558 -0
  137. package/src/core/voice.ts +321 -0
  138. package/src/core/zod.ts +79 -0
  139. package/src/runtime/browser.ts +203 -0
  140. package/src/runtime/clone.ts +52 -0
  141. package/src/runtime/deck-control.ts +43 -0
  142. package/src/runtime/fit.ts +32 -0
  143. package/src/runtime/go.ts +56 -0
  144. package/src/runtime/input.ts +128 -0
  145. package/src/runtime/laser-view.ts +110 -0
  146. package/src/runtime/laser.ts +145 -0
  147. package/src/runtime/live-host.ts +33 -0
  148. package/src/runtime/live-reload.ts +29 -0
  149. package/src/runtime/live.ts +88 -0
  150. package/src/runtime/marks-view.ts +74 -0
  151. package/src/runtime/motion.ts +113 -0
  152. package/src/runtime/navigator.ts +189 -0
  153. package/src/runtime/player.ts +28 -0
  154. package/src/runtime/presenter-view.ts +289 -0
  155. package/src/runtime/presenter.ts +52 -0
  156. package/src/runtime/rail-view.ts +229 -0
  157. package/src/runtime/rail.ts +52 -0
  158. package/src/runtime/rehearse.ts +264 -0
  159. package/src/runtime/routes.ts +142 -0
  160. package/src/runtime/settle.ts +45 -0
  161. package/src/runtime/slide.d.ts +28 -0
  162. package/src/runtime/socket.ts +143 -0
  163. package/src/runtime/stage.ts +259 -0
  164. package/src/runtime/step.ts +139 -0
  165. package/src/runtime/window.ts +38 -0
  166. package/src/server/auth.ts +210 -0
  167. package/src/server/deck-snapshot.ts +146 -0
  168. package/src/server/dev.ts +251 -0
  169. package/src/server/hub.ts +104 -0
  170. package/src/server/lan.ts +51 -0
  171. package/src/server/lock.ts +97 -0
  172. package/src/server/pages.ts +71 -0
  173. package/src/server/pairing.ts +40 -0
  174. package/src/server/project-watch.ts +142 -0
  175. package/src/server/rooms.ts +133 -0
  176. package/src/server/routes.ts +286 -0
  177. package/src/server/serial.ts +25 -0
  178. package/src/server/watch.ts +308 -0
  179. package/src/theme/default.css +175 -0
  180. package/src/video/bake.ts +152 -0
  181. package/src/video/mux.ts +103 -0
  182. package/src/video/recorder.ts +115 -0
  183. package/src/video/sidecar.ts +60 -0
  184. package/src/video/worker.ts +51 -0
  185. package/src/voice/engine.ts +177 -0
  186. package/src/voice/synth.ts +127 -0
  187. package/src/voice/wav.ts +115 -0
@@ -0,0 +1,127 @@
1
+ /**
2
+ * A declaration's value as CSS reads it: component values, after CSS Syntax Level 3. A number
3
+ * keeps its sign and its unit, a function holds its arguments as values, and a string or a
4
+ * comment is never read as anything else. Rules ask what a value is made of, not what text it
5
+ * matches, so `-12px` is a length and `tan` in `grid-area` is only a name.
6
+ */
7
+ export type CssValue =
8
+ | { kind: "ident"; value: string }
9
+ | { kind: "function"; name: string; args: CssValue[] }
10
+ | { kind: "number"; value: number }
11
+ | { kind: "percentage"; value: number }
12
+ | { kind: "dimension"; value: number; unit: string }
13
+ | { kind: "hash"; value: string }
14
+ | { kind: "string"; value: string }
15
+ | { kind: "url"; value: string }
16
+ | { kind: "delim"; value: string };
17
+
18
+ const NUMBER_RE = /^[+-]?(?:\d+(?:\.\d*)?|\.\d+)(?:[eE][+-]?\d+)?/;
19
+ const NAME_CHAR_RE = /[-\w\u0080-\uffff]/;
20
+
21
+ /** Whether `text` at `at` starts an identifier: a letter, `_`, `-` then one, `--`, or `\`. */
22
+ function startsIdent(text: string, at: number): boolean {
23
+ const first = text[at] ?? "";
24
+ const second = text[at + 1] ?? "";
25
+ const letter = (ch: string): boolean => /[A-Za-z_\u0080-\uffff\\]/.test(ch);
26
+ return first === "-" ? letter(second) || second === "-" : letter(first);
27
+ }
28
+
29
+ /** Every component value in `text`, spaces and comments dropped. */
30
+ export function parseCssValue(text: string): CssValue[] {
31
+ return new ValueReader(text).values(false);
32
+ }
33
+
34
+ class ValueReader {
35
+ at = 0;
36
+
37
+ constructor(readonly text: string) {}
38
+
39
+ /** The values up to the end, or up to the `)` that closes the function being read. */
40
+ values(nested: boolean): CssValue[] {
41
+ const out: CssValue[] = [];
42
+ while (this.at < this.text.length) {
43
+ const ch = this.text[this.at] ?? "";
44
+ if (/\s/.test(ch)) {
45
+ this.at++;
46
+ } else if (ch === "/" && this.text[this.at + 1] === "*") {
47
+ const close = this.text.indexOf("*/", this.at + 2);
48
+ this.at = close < 0 ? this.text.length : close + 2;
49
+ } else if (ch === ")") {
50
+ if (nested) {
51
+ return out;
52
+ }
53
+ this.at++;
54
+ } else {
55
+ out.push(this.value());
56
+ }
57
+ }
58
+ return out;
59
+ }
60
+
61
+ value(): CssValue {
62
+ const { text } = this;
63
+ const ch = text[this.at] ?? "";
64
+ if (ch === '"' || ch === "'") {
65
+ return { kind: "string", value: this.string(ch) };
66
+ }
67
+ if (ch === "#" && NAME_CHAR_RE.test(text[this.at + 1] ?? "")) {
68
+ this.at++;
69
+ return { kind: "hash", value: this.name() };
70
+ }
71
+ const number = NUMBER_RE.exec(text.slice(this.at));
72
+ if (number) {
73
+ this.at += number[0].length;
74
+ const value = Number(number[0]);
75
+ if (text[this.at] === "%") {
76
+ this.at++;
77
+ return { kind: "percentage", value };
78
+ }
79
+ return startsIdent(text, this.at)
80
+ ? { kind: "dimension", value, unit: this.name() }
81
+ : { kind: "number", value };
82
+ }
83
+ if (startsIdent(text, this.at)) {
84
+ const name = this.name();
85
+ if (text[this.at] !== "(") {
86
+ return { kind: "ident", value: name };
87
+ }
88
+ this.at++;
89
+ if (name.toLowerCase() === "url" && !/^\s*["']/.test(text.slice(this.at))) {
90
+ const close = text.indexOf(")", this.at);
91
+ const value = text.slice(this.at, close < 0 ? text.length : close).trim();
92
+ this.at = close < 0 ? text.length : close + 1;
93
+ return { kind: "url", value };
94
+ }
95
+ const args = this.values(true);
96
+ this.at++;
97
+ return { kind: "function", name: name.toLowerCase(), args };
98
+ }
99
+ this.at++;
100
+ return { kind: "delim", value: ch };
101
+ }
102
+
103
+ name(): string {
104
+ const start = this.at;
105
+ while (this.at < this.text.length) {
106
+ const ch = this.text[this.at] ?? "";
107
+ if (ch === "\\") {
108
+ this.at += 2;
109
+ } else if (NAME_CHAR_RE.test(ch)) {
110
+ this.at++;
111
+ } else {
112
+ break;
113
+ }
114
+ }
115
+ return this.text.slice(start, this.at);
116
+ }
117
+
118
+ string(quote: string): string {
119
+ const start = ++this.at;
120
+ while (this.at < this.text.length && this.text[this.at] !== quote) {
121
+ this.at += this.text[this.at] === "\\" ? 2 : 1;
122
+ }
123
+ const value = this.text.slice(start, this.at);
124
+ this.at++;
125
+ return value;
126
+ }
127
+ }
@@ -0,0 +1,545 @@
1
+ import {
2
+ blankStringsAndComments,
3
+ consumeComment,
4
+ consumeString,
5
+ matchBrace,
6
+ scanTopLevel,
7
+ splitTopLevel,
8
+ stripComments,
9
+ } from "./css-scan.ts";
10
+ import { lineLocator } from "./lines.ts";
11
+
12
+ export type CssDeclaration = {
13
+ /**
14
+ * The rule's selector, with nesting resolved: `.slide { &:hover {} }` gives `.slide:hover`.
15
+ * A descriptor outside every style rule, such as `@font-face`'s, has none: "".
16
+ */
17
+ selector: string;
18
+ property: string;
19
+ value: string;
20
+ line: number;
21
+ /** Where the value is written in the stylesheet: `[valueStart, valueEnd)`, spaces trimmed. */
22
+ valueStart: number;
23
+ valueEnd: number;
24
+ /** The at-rules around the declaration, outermost first, such as `@media (…)`. */
25
+ atPath: string[];
26
+ };
27
+
28
+ type RuleBase = {
29
+ /** The selector or the at-rule's prelude as written, comments dropped: `&:hover`, `@media (…)`. */
30
+ prelude: string;
31
+ /** The line the prelude starts on. */
32
+ line: number;
33
+ /** Where the prelude is written, spaces trimmed. */
34
+ preludeStart: number;
35
+ preludeEnd: number;
36
+ /** The block's inside; `bodyEnd` is its closing `}`, or the end of an unclosed one. */
37
+ bodyStart: number;
38
+ bodyEnd: number;
39
+ /** The at-rules around the rule, outermost first. */
40
+ atPath: string[];
41
+ /** The style rule this one is nested in. */
42
+ parent?: StyleRule;
43
+ /** A keyframe stop, or an at-rule inside `@keyframes`. */
44
+ keyframe: boolean;
45
+ /** The declarations written directly in the block, not in blocks nested in it. */
46
+ decls: CssDeclaration[];
47
+ };
48
+
49
+ export type StyleRule = RuleBase & {
50
+ kind: "style";
51
+ /** The selector with nesting resolved; a keyframe stop's is its prelude. */
52
+ selector: string;
53
+ };
54
+
55
+ /**
56
+ * An at-rule with a block. One nested in a style rule, such as `.card { @media (…) { … } }`,
57
+ * styles that rule's elements: its `parent` names them.
58
+ */
59
+ type AtRule = RuleBase & {
60
+ kind: "at";
61
+ /** A block of descriptors, such as `@font-face`, instead of rules or declarations. */
62
+ descriptors: boolean;
63
+ };
64
+
65
+ type CssRule = StyleRule | AtRule;
66
+
67
+ /** One `url()`, the address it names and the span of the whole token. */
68
+ type CssUrl = { value: string; line: number; start: number; end: number };
69
+
70
+ /** A stylesheet read once; every question css.ts answers is a look at this. */
71
+ export type Stylesheet = {
72
+ source: string;
73
+ /** Every rule at any depth, style and at-rules alike, in source order. */
74
+ rules: CssRule[];
75
+ /** The declarations of style rules and keyframe stops, nested ones included, in source order. */
76
+ decls: CssDeclaration[];
77
+ /** The descriptors of `@font-face` and its kind. */
78
+ descriptors: CssDeclaration[];
79
+ /** Every at-rule, block or statement, by name without the `@`, with the line it starts on. */
80
+ atRules: Array<{ name: string; start: number; line: number }>;
81
+ /**
82
+ * Every `url()` in a declaration or descriptor value, and every address `@import` loads; a
83
+ * `url(` inside a string is text.
84
+ */
85
+ urls: CssUrl[];
86
+ /** Where each `@keyframes` name is written. */
87
+ keyframes: Array<{ name: string; start: number; end: number }>;
88
+ };
89
+
90
+ const KEYFRAMES_RE = /^@(-\w+-)?keyframes\b/i;
91
+ /** At-rules whose block holds descriptors, written like declarations, instead of rules. */
92
+ const DESCRIPTOR_AT_RULE_RE = /^@(font-face|page|property|counter-style|font-palette-values)\b/i;
93
+ const WHITESPACE_RE = /\s/;
94
+
95
+ type Scope = {
96
+ atPath: string[];
97
+ parent?: StyleRule;
98
+ /** The rule whose block this is, which the block's declarations belong to. */
99
+ owner?: CssRule;
100
+ keyframe: boolean;
101
+ /** What the block holds: rules (a sheet, `@media` at the top), declarations, or descriptors. */
102
+ holds: "rules" | "decls" | "descriptors";
103
+ };
104
+
105
+ /** Reads a stylesheet once: rules with their nesting, declarations, at-rules, and `url()`s. */
106
+ export function parseCss(source: string): Stylesheet {
107
+ const sheet: Stylesheet = {
108
+ source,
109
+ rules: [],
110
+ decls: [],
111
+ descriptors: [],
112
+ atRules: [],
113
+ urls: [],
114
+ keyframes: [],
115
+ };
116
+ const reader = new SheetReader(sheet);
117
+ reader.block(0, source.length, { atPath: [], keyframe: false, holds: "rules" });
118
+ return sheet;
119
+ }
120
+
121
+ class SheetReader {
122
+ readonly source: string;
123
+ readonly blank: string;
124
+ readonly lineOf: (offset: number) => number;
125
+ /** Where each comment is written, in order. */
126
+ readonly comments: Array<[number, number]> = [];
127
+
128
+ constructor(readonly sheet: Stylesheet) {
129
+ this.source = sheet.source;
130
+ this.blank = blankStringsAndComments(sheet.source);
131
+ const { source } = this;
132
+ let i = 0;
133
+ while (i < source.length) {
134
+ const ch = source[i];
135
+ if (ch === '"' || ch === "'") {
136
+ i = consumeString(source, i);
137
+ } else if (ch === "/" && source[i + 1] === "*") {
138
+ const close = consumeComment(source, i);
139
+ this.comments.push([i, close]);
140
+ i = close;
141
+ } else {
142
+ i += ch === "\\" ? 2 : 1;
143
+ }
144
+ }
145
+ const locate = lineLocator(sheet.source);
146
+ this.lineOf = (offset) => locate(offset).line;
147
+ }
148
+
149
+ block(start: number, end: number, scope: Scope): void {
150
+ const { source } = this;
151
+ let i = start;
152
+ while (i < end) {
153
+ i = this.skipSpace(i, end);
154
+ const ch = source[i];
155
+ if (i >= end) {
156
+ break;
157
+ }
158
+ if (ch === "}" || ch === ";") {
159
+ i++;
160
+ } else if (ch === "@") {
161
+ i = this.atRule(i, end, scope);
162
+ } else if (scope.holds === "rules") {
163
+ i = this.styleRule(i, scanTopLevel(source, i, end, "{}"), end, scope);
164
+ } else {
165
+ i = this.declarationOrRule(i, end, scope);
166
+ }
167
+ }
168
+ }
169
+
170
+ skipSpace(from: number, end: number): number {
171
+ let i = from;
172
+ while (i < end) {
173
+ if (WHITESPACE_RE.test(this.source[i] ?? "")) {
174
+ i++;
175
+ } else if (this.source[i] === "/" && this.source[i + 1] === "*") {
176
+ i = consumeComment(this.source, i, end);
177
+ } else {
178
+ break;
179
+ }
180
+ }
181
+ return i;
182
+ }
183
+
184
+ /** A prelude's span with its trailing spaces trimmed off. */
185
+ trimEnd(start: number, end: number): number {
186
+ let j = end;
187
+ while (j > start && WHITESPACE_RE.test(this.blank[j - 1] ?? "")) {
188
+ j--;
189
+ }
190
+ return j;
191
+ }
192
+
193
+ atRule(start: number, end: number, scope: Scope): number {
194
+ const { source, sheet } = this;
195
+ const name = /^@([-a-zA-Z]+)/.exec(source.slice(start, start + 64))?.[1];
196
+ if (name) {
197
+ sheet.atRules.push({ name, start, line: this.lineOf(start) });
198
+ }
199
+ const stop = scanTopLevel(source, start, end, ";{}");
200
+ if (source[stop] !== "{") {
201
+ if (name?.toLowerCase() === "import") {
202
+ this.importAddress(start + name.length + 1, stop);
203
+ }
204
+ return source[stop] === ";" ? stop + 1 : stop;
205
+ }
206
+ const close = matchBrace(source, stop, end);
207
+ const preludeEnd = this.trimEnd(start, stop);
208
+ const prelude = stripComments(source.slice(start, preludeEnd)).trim();
209
+ const descriptors = DESCRIPTOR_AT_RULE_RE.test(prelude);
210
+ const keyframes = KEYFRAMES_RE.test(prelude);
211
+ const rule: AtRule = {
212
+ kind: "at",
213
+ prelude,
214
+ line: this.lineOf(start),
215
+ preludeStart: start,
216
+ preludeEnd,
217
+ bodyStart: stop + 1,
218
+ bodyEnd: close,
219
+ atPath: scope.atPath,
220
+ ...(scope.parent ? { parent: scope.parent } : {}),
221
+ descriptors,
222
+ keyframe: scope.keyframe,
223
+ decls: [],
224
+ };
225
+ sheet.rules.push(rule);
226
+ if (keyframes) {
227
+ this.keyframesName(start + (name?.length ?? 0) + 1, stop);
228
+ }
229
+ this.block(stop + 1, close, {
230
+ atPath: [...scope.atPath, prelude],
231
+ ...(scope.parent ? { parent: scope.parent } : {}),
232
+ owner: rule,
233
+ keyframe: scope.keyframe || keyframes,
234
+ holds: descriptors ? "descriptors" : keyframes || scope.holds === "rules" ? "rules" : "decls",
235
+ });
236
+ return close + 1;
237
+ }
238
+
239
+ keyframesName(from: number, end: number): void {
240
+ const at = this.skipSpace(from, end);
241
+ const quote = this.source[at];
242
+ if (quote === '"' || quote === "'") {
243
+ const close = consumeString(this.source, at, end);
244
+ this.sheet.keyframes.push({
245
+ name: this.source.slice(at + 1, close - 1),
246
+ start: at + 1,
247
+ end: close - 1,
248
+ });
249
+ return;
250
+ }
251
+ const ident = /^[-_a-zA-Z][-\w]*/.exec(this.source.slice(at, end));
252
+ if (ident) {
253
+ this.sheet.keyframes.push({ name: ident[0], start: at, end: at + ident[0].length });
254
+ }
255
+ }
256
+
257
+ /** A style rule whose prelude runs from `start` to the `{` at `open`. */
258
+ styleRule(start: number, open: number, end: number, scope: Scope): number {
259
+ const { source } = this;
260
+ if (source[open] !== "{") {
261
+ return open;
262
+ }
263
+ const close = matchBrace(source, open, end);
264
+ const preludeEnd = this.trimEnd(start, open);
265
+ const prelude = stripComments(source.slice(start, preludeEnd)).trim();
266
+ const rule: StyleRule = {
267
+ kind: "style",
268
+ prelude,
269
+ line: this.lineOf(start),
270
+ preludeStart: start,
271
+ preludeEnd,
272
+ bodyStart: open + 1,
273
+ bodyEnd: close,
274
+ selector: scope.keyframe ? prelude : resolveNesting(prelude, scope.parent?.selector),
275
+ atPath: scope.atPath,
276
+ ...(scope.parent ? { parent: scope.parent } : {}),
277
+ keyframe: scope.keyframe,
278
+ decls: [],
279
+ };
280
+ this.sheet.rules.push(rule);
281
+ this.block(open + 1, close, {
282
+ atPath: scope.atPath,
283
+ parent: rule,
284
+ owner: rule,
285
+ keyframe: scope.keyframe,
286
+ holds: "decls",
287
+ });
288
+ return close + 1;
289
+ }
290
+
291
+ /**
292
+ * In a block of declarations, `color: red` is a declaration and `&:hover { … }` a nested rule:
293
+ * a `{` outside parentheses makes it a rule, unless it sits in a custom property's value.
294
+ */
295
+ declarationOrRule(start: number, end: number, scope: Scope): number {
296
+ const { source } = this;
297
+ const colon = scanTopLevel(source, start, end, ":;{}");
298
+ if (source[colon] === "{") {
299
+ return scope.holds === "decls"
300
+ ? this.styleRule(start, colon, end, scope)
301
+ : matchBrace(source, colon, end) + 1;
302
+ }
303
+ if (source[colon] !== ":") {
304
+ return source[colon] === ";" ? colon + 1 : colon;
305
+ }
306
+ const property = stripComments(source.slice(start, colon)).trim();
307
+ let stop = scanTopLevel(source, colon + 1, end, ";{}");
308
+ while (source[stop] === "{") {
309
+ if (!property.startsWith("--")) {
310
+ return scope.holds === "decls"
311
+ ? this.styleRule(start, stop, end, scope)
312
+ : matchBrace(source, stop, end) + 1;
313
+ }
314
+ stop = scanTopLevel(source, matchBrace(source, stop, end) + 1, end, ";{}");
315
+ }
316
+ const valueStart = this.skipSpace(colon + 1, stop);
317
+ const valueEnd = Math.max(valueStart, this.trimEnd(colon + 1, stop));
318
+ if (property && scope.owner) {
319
+ const decl: CssDeclaration = {
320
+ selector: ruleSelector(scope.owner),
321
+ property,
322
+ value: this.valueText(colon + 1, stop),
323
+ line: this.lineOf(start),
324
+ valueStart,
325
+ valueEnd,
326
+ atPath: scope.atPath,
327
+ };
328
+ scope.owner.decls.push(decl);
329
+ (scope.holds === "descriptors" ? this.sheet.descriptors : this.sheet.decls).push(decl);
330
+ this.urls(valueStart, valueEnd);
331
+ }
332
+ return source[stop] === ";" ? stop + 1 : stop;
333
+ }
334
+
335
+ /** A value as the browser reads it: each comment a space, strings as written. */
336
+ valueText(start: number, end: number): string {
337
+ let out = "";
338
+ let from = start;
339
+ for (const [open, close] of this.comments) {
340
+ if (open >= start && open < end) {
341
+ out += `${this.source.slice(from, open)} `;
342
+ from = Math.min(close, end);
343
+ }
344
+ }
345
+ return (out + this.source.slice(from, end)).trim();
346
+ }
347
+
348
+ /** What `@import` loads: a `url()`, or a bare string, which names an address all the same. */
349
+ importAddress(from: number, end: number): void {
350
+ const at = this.skipSpace(from, end);
351
+ const quote = this.source[at];
352
+ if (quote !== '"' && quote !== "'") {
353
+ this.urls(at, end);
354
+ return;
355
+ }
356
+ const close = consumeString(this.source, at, end);
357
+ this.sheet.urls.push({
358
+ value: this.source.slice(at + 1, close - 1).trim(),
359
+ line: this.lineOf(at),
360
+ start: at,
361
+ end: close,
362
+ });
363
+ }
364
+
365
+ urls(start: number, end: number): void {
366
+ const syntax = this.blank.slice(start, end);
367
+ // image-set() takes its addresses as strings as well as url()s.
368
+ for (const match of syntax.matchAll(/(?<![-\w])(?:-webkit-)?image-set\(/gi)) {
369
+ const open = start + match.index + match[0].length - 1;
370
+ const close = matchBrace(this.source.replace(/\(/g, "{").replace(/\)/g, "}"), open, end);
371
+ for (let at = open + 1; at < close; at++) {
372
+ const quote = this.source[at];
373
+ if ((quote === '"' || quote === "'") && this.blank[at] === " ") {
374
+ const stop = consumeString(this.source, at, close);
375
+ this.sheet.urls.push({
376
+ value: this.source.slice(at + 1, stop - 1).trim(),
377
+ line: this.lineOf(at),
378
+ start: at,
379
+ end: stop,
380
+ });
381
+ at = stop - 1;
382
+ }
383
+ }
384
+ }
385
+ for (const match of syntax.matchAll(/(?<![-\w])url\(/gi)) {
386
+ const at = start + match.index;
387
+ // The address itself may be quoted, so it is read from the source, not the blanked copy.
388
+ const token = /^url\(\s*(["']?)([^"')]*)\1\s*\)/i.exec(this.source.slice(at, end));
389
+ if (token) {
390
+ this.sheet.urls.push({
391
+ value: (token[2] ?? "").trim(),
392
+ line: this.lineOf(at),
393
+ start: at,
394
+ end: at + token[0].length,
395
+ });
396
+ }
397
+ }
398
+ }
399
+ }
400
+
401
+ /** The elements a rule's declarations style: an at-rule styles its parent's, if any. */
402
+ function ruleSelector(rule: CssRule): string {
403
+ return rule.kind === "style" ? rule.selector : (rule.parent?.selector ?? "");
404
+ }
405
+
406
+ /**
407
+ * A nested selector read against its parent's, as CSS nesting reads it: `&` stands for the
408
+ * parent, and a part without one is a descendant of it.
409
+ */
410
+ function resolveNesting(selector: string, parent: string | undefined): string {
411
+ if (parent === undefined) {
412
+ return selector;
413
+ }
414
+ const parents = splitSelectorList(parent)
415
+ .map((part) => part.trim())
416
+ .filter(Boolean);
417
+ const self = parents.length === 1 ? (parents[0] ?? "") : `:is(${parents.join(", ")})`;
418
+ return splitSelectorList(selector)
419
+ .map((part) => part.trim())
420
+ .filter(Boolean)
421
+ .map((part) => (part.includes("&") ? part.replaceAll("&", self) : `${self} ${part}`))
422
+ .join(", ");
423
+ }
424
+
425
+ /** Style rules, nested ones included, that are not keyframe stops. */
426
+ function styleRules(sheet: Stylesheet): StyleRule[] {
427
+ return sheet.rules.filter((rule): rule is StyleRule => rule.kind === "style" && !rule.keyframe);
428
+ }
429
+
430
+ /**
431
+ * The style rules nested in no other style rule, those inside `@media` or `@supports`
432
+ * included, and not keyframe stops: the rules whose selectors reach the page as written.
433
+ */
434
+ export function outermostStyleRules(sheet: Stylesheet): StyleRule[] {
435
+ return styleRules(sheet).filter((rule) => !rule.parent);
436
+ }
437
+
438
+ const CLASS_RE = /\.(-?[_a-zA-Z]+[_a-zA-Z0-9-]*)/g;
439
+ const LAYOUT_ATTR_RE = /\[data-layout\s*=\s*(["']?)([^\]"'\s]+)\1\]/g;
440
+
441
+ /** The classes a selector names; a dot inside a string or an attribute value is no class. */
442
+ export function selectorClasses(selector: string): string[] {
443
+ return [...blankStringsAndComments(selector).matchAll(CLASS_RE)].flatMap((match) =>
444
+ match[1] ? [match[1]] : [],
445
+ );
446
+ }
447
+
448
+ /** The layouts a selector names through `[data-layout=...]`. */
449
+ export function selectorLayouts(selector: string): string[] {
450
+ return [...selector.matchAll(LAYOUT_ATTR_RE)].flatMap((match) => (match[2] ? [match[2]] : []));
451
+ }
452
+
453
+ export function cssClassNames(sheet: Stylesheet): Set<string> {
454
+ return new Set(styleRules(sheet).flatMap((rule) => selectorClasses(rule.prelude)));
455
+ }
456
+
457
+ /** The layouts the stylesheet's selectors name through `[data-layout=...]`. */
458
+ export function cssLayoutNames(sheet: Stylesheet): Set<string> {
459
+ return new Set(styleRules(sheet).flatMap((rule) => selectorLayouts(rule.prelude)));
460
+ }
461
+
462
+ /** A selector as a rule writes it, with the line the rule starts on. */
463
+ export type SelectorAt = { selector: string; line: number };
464
+
465
+ /** Style rule selectors at any depth, nesting resolved, skipping keyframe stops. */
466
+ export function cssStyleSelectors(sheet: Stylesheet): SelectorAt[] {
467
+ return styleRules(sheet).map((rule) => ({ selector: rule.selector, line: rule.line }));
468
+ }
469
+
470
+ /**
471
+ * Every at-rule in the stylesheet, such as `font-face` or `media`, by name, with its line, in
472
+ * source order; strings are skipped.
473
+ */
474
+ export function cssAtRules(sheet: Stylesheet): Array<{ name: string; line: number }> {
475
+ return sheet.atRules.map(({ name, line }) => ({ name, line }));
476
+ }
477
+
478
+ /**
479
+ * Each `url()` a declaration or an `@font-face`-like descriptor references, with its line; a
480
+ * `url(` inside a string is text.
481
+ */
482
+ export function cssUrls(sheet: Stylesheet): Array<{ value: string; line: number }> {
483
+ return sheet.urls.map(({ value, line }) => ({ value, line }));
484
+ }
485
+
486
+ /**
487
+ * The selectors that reach the page on their own: every outermost style rule, but not those
488
+ * under `@scope`, which names its own root.
489
+ */
490
+ export function outermostSelectors(sheet: Stylesheet): SelectorAt[] {
491
+ return outermostStyleRules(sheet)
492
+ .filter((rule) => !rule.atPath.some((at) => /^@scope\b/i.test(at)))
493
+ .map((rule) => ({ selector: rule.prelude, line: rule.line }));
494
+ }
495
+
496
+ /** Whether each part of a selector list starts at `.slide`, or is a view-transition pseudo-element. */
497
+ export function isScopedThemeSelector(selector: string): boolean {
498
+ return splitSelectorList(selector).every((part) => {
499
+ const item = part.trim();
500
+ return item === "" || item.startsWith("::view-transition-") || startsAtSlide(item);
501
+ });
502
+ }
503
+
504
+ /** Splits `a, b` on top-level commas only, leaving `:is(.a, .b)` whole. */
505
+ export function splitSelectorList(selector: string): string[] {
506
+ return splitTopLevel(selector, ",");
507
+ }
508
+
509
+ /** A selector part that starts at the slide itself, not at a class that merely begins with "slide". */
510
+ export function startsAtSlide(part: string): boolean {
511
+ return /^\.slide(?=$|[\s[.:#>+~])/.test(part);
512
+ }
513
+
514
+ /** Whether an at-rule prelude opens `@keyframes`, vendor-prefixed or not. */
515
+ export function isKeyframesPrelude(prelude: string): boolean {
516
+ return KEYFRAMES_RE.test(prelude);
517
+ }
518
+
519
+ /** A `--*` token and the value assigned to it. */
520
+ export type CssToken = { name: string; value: string };
521
+
522
+ /**
523
+ * The tokens every slide can `var()`: the ones the bare `.slide` rule sets outside any at-rule,
524
+ * first assignment of each name, in order. One set only on a layout, inside one element, under
525
+ * `@media`, or on a view transition is no token a slide can count on.
526
+ */
527
+ export function publishedTokens(sheet: Stylesheet): CssToken[] {
528
+ return firstAssignments(
529
+ sheet.decls.filter(
530
+ (decl) =>
531
+ decl.selector === ".slide" && decl.atPath.length === 0 && decl.property.startsWith("--"),
532
+ ),
533
+ );
534
+ }
535
+
536
+ function firstAssignments(decls: CssDeclaration[]): CssToken[] {
537
+ const seen = new Set<string>();
538
+ return decls.flatMap((decl) => {
539
+ if (seen.has(decl.property)) {
540
+ return [];
541
+ }
542
+ seen.add(decl.property);
543
+ return [{ name: decl.property, value: decl.value }];
544
+ });
545
+ }