@gtkx/css 1.2.2 → 1.4.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/README.md CHANGED
@@ -5,9 +5,9 @@
5
5
  <h1 align="center">GTKX</h1>
6
6
 
7
7
  <p align="center">
8
- The React framework for Linux.<br />
9
- Write native Linux applications with React and TypeScript.
10
- Built on the GNOME stack and standard web tooling.
8
+ The React framework for Linux, built on GTK.<br />
9
+ Write native GTK4 desktop applications with React and TypeScript.
10
+ Real GTK widgets, the GNOME stack, and standard web tooling.
11
11
  </p>
12
12
 
13
13
  <p align="center">
@@ -98,7 +98,7 @@ GTK4 is mature, and GtkBuilder XML can lay out a static interface, but nothing r
98
98
  - a React reconciler that exposes every GObject as a JSX element,
99
99
  - a CLI for scaffolding, development, and production builds,
100
100
  - a dev server with Fast Refresh that patches your running UI in place,
101
- - CSS-in-JS styling and high-level list, grid, and dialog components,
101
+ - CSS-in-JS styling, React Spring animations, React Navigation stack, tab, drawer, and split view navigators, and high-level list, grid, and dialog components,
102
102
  - a Testing Library-style API for querying and driving your widgets in tests,
103
103
  - and a Model Context Protocol (MCP) server that exposes your live app to AI agents.
104
104
 
@@ -157,6 +157,8 @@ Explore the [example apps](https://github.com/gtkx-org/gtkx/tree/main/examples):
157
157
  - [`hello-world`](https://github.com/gtkx-org/gtkx/tree/main/examples/hello-world): the counter above.
158
158
  - [`gtk-demo`](https://github.com/gtkx-org/gtkx/tree/main/examples/gtk-demo): a React port of the official GTK4 widget showcase, covering lists, dialogs, gestures, CSS, and OpenGL.
159
159
  - [`browser`](https://github.com/gtkx-org/gtkx/tree/main/examples/browser): a WebKitWebView-based web browser.
160
+ - [`animations`](https://github.com/gtkx-org/gtkx/tree/main/examples/animations): a tour of `@gtkx/animated`, React Spring animations driven by the GTK frame clock.
161
+ - [`navigation`](https://github.com/gtkx-org/gtkx/tree/main/examples/navigation): a tour of `@gtkx/navigation`, React Navigation's stack, tab, and drawer navigators rendered with libadwaita.
160
162
  - [`tutorial`](https://github.com/gtkx-org/gtkx/tree/main/examples/tutorial): the Tasks app the documentation builds.
161
163
 
162
164
  ## Status
@@ -1 +1 @@
1
- {"version":3,"file":"create-css.d.ts","sourceRoot":"","sources":["../src/create-css.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,gBAAgB,EAAqC,MAAM,oBAAoB,CAAC;AAO9F,8FAA8F;AAC9F,KAAK,OAAO,GAAG,MAAM,GAAG,OAAO,GAAG,SAAS,GAAG,IAAI,CAAC;AAEnD,gHAAgH;AAChH,KAAK,GAAG,GAAG;IACP;;;OAGG;IACH,GAAG,EAAE,CAAC,GAAG,IAAI,EAAE,gBAAgB,EAAE,KAAK,MAAM,CAAC;IAC7C;;;OAGG;IACH,EAAE,EAAE,CAAC,GAAG,UAAU,EAAE,OAAO,EAAE,KAAK,MAAM,EAAE,CAAC;IAC3C,mGAAmG;IACnG,YAAY,EAAE,CAAC,GAAG,IAAI,EAAE,gBAAgB,EAAE,KAAK,IAAI,CAAC;CACvD,CAAC;AA+GF,QAAA,MAAM,SAAS,QAAO,GAUrB,CAAC;AAEF,OAAO,EAAE,SAAS,EAAE,KAAK,GAAG,EAAE,CAAC"}
1
+ {"version":3,"file":"create-css.d.ts","sourceRoot":"","sources":["../src/create-css.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,gBAAgB,EAAqC,MAAM,oBAAoB,CAAC;AAK9F,8FAA8F;AAC9F,KAAK,OAAO,GAAG,MAAM,GAAG,OAAO,GAAG,SAAS,GAAG,IAAI,CAAC;AAEnD,gHAAgH;AAChH,KAAK,GAAG,GAAG;IACP;;;OAGG;IACH,GAAG,EAAE,CAAC,GAAG,IAAI,EAAE,gBAAgB,EAAE,KAAK,MAAM,CAAC;IAC7C;;;OAGG;IACH,EAAE,EAAE,CAAC,GAAG,UAAU,EAAE,OAAO,EAAE,KAAK,MAAM,EAAE,CAAC;IAC3C,mGAAmG;IACnG,YAAY,EAAE,CAAC,GAAG,IAAI,EAAE,gBAAgB,EAAE,KAAK,IAAI,CAAC;CACvD,CAAC;AAgFF,QAAA,MAAM,SAAS,QAAO,GAUrB,CAAC;AAEF,OAAO,EAAE,SAAS,EAAE,KAAK,GAAG,EAAE,CAAC"}
@@ -1,27 +1,8 @@
1
1
  import { serializeStyles } from "@emotion/serialize";
2
- import { compile, middleware, rulesheet, stringify, serialize as stylisSerialize } from "stylis";
3
- import { escapeNamedColors, restoreNamedColors } from "./named-colors.js";
2
+ import { eachRule, terminateDeclarations } from "./serialize-rule.js";
4
3
  import { StyleSheet } from "./stylesheet.js";
5
4
  const KEY = "gtkx";
6
- const LABEL_DECL_FIRST_CHAR = 108;
7
- const LABEL_DECL_THIRD_CHAR = 98;
8
- const removeLabel = (element) => {
9
- if (!(element.type === "decl" &&
10
- element.value.codePointAt(0) === LABEL_DECL_FIRST_CHAR &&
11
- element.value.codePointAt(2) === LABEL_DECL_THIRD_CHAR)) {
12
- return;
13
- }
14
- element.return = "";
15
- element.value = "";
16
- };
17
5
  const getClassName = (serialized) => `${KEY}-${serialized.name}`;
18
- const terminateDeclarations = (styles) => {
19
- const trimmed = styles.trimEnd();
20
- if (trimmed.length === 0 || trimmed.endsWith(";") || trimmed.endsWith("}")) {
21
- return trimmed;
22
- }
23
- return `${trimmed};`;
24
- };
25
6
  const isNonEmptyString = (token) => typeof token === "string" && token.length > 0;
26
7
  const partitionTokens = (tokens, registered) => {
27
8
  const rawClasses = [];
@@ -37,14 +18,10 @@ const partitionTokens = (tokens, registered) => {
37
18
  }
38
19
  return { rawClasses, registeredStyles };
39
20
  };
40
- const runStylis = (sheet, input) => {
41
- stylisSerialize(compile(escapeNamedColors(input)), middleware([
42
- removeLabel,
43
- stringify,
44
- rulesheet((rule) => {
45
- sheet.insert(restoreNamedColors(rule));
46
- }),
47
- ]));
21
+ const insertRules = (state, input) => {
22
+ eachRule(input, (rule) => {
23
+ state.sheet.insert(rule);
24
+ });
48
25
  };
49
26
  const didMarkNewStyle = (state, serialized) => {
50
27
  if (state.inserted.has(serialized.name)) {
@@ -59,14 +36,14 @@ const insertStyles = (state, serialized) => {
59
36
  }
60
37
  const className = getClassName(serialized);
61
38
  const styles = terminateDeclarations(serialized.styles);
62
- runStylis(state.sheet, `.${className}{${styles}}`);
39
+ insertRules(state, `.${className}{${styles}}`);
63
40
  state.registered[className] = styles;
64
41
  };
65
42
  const insertWithoutScoping = (state, serialized) => {
66
43
  if (!didMarkNewStyle(state, serialized)) {
67
44
  return;
68
45
  }
69
- runStylis(state.sheet, serialized.styles);
46
+ insertRules(state, serialized.styles);
70
47
  };
71
48
  const cssClassName = (state, args) => {
72
49
  const serialized = serializeStyles(args, state.registered);
@@ -1 +1 @@
1
- {"version":3,"file":"create-css.js","sourceRoot":"","sources":["../src/create-css.ts"],"names":[],"mappings":"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","sourcesContent":["import type { CSSInterpolation, RegisteredCache, SerializedStyles } from \"@emotion/serialize\";\nimport type { Element } from \"stylis\";\nimport { serializeStyles } from \"@emotion/serialize\";\nimport { compile, middleware, rulesheet, stringify, serialize as stylisSerialize } from \"stylis\";\nimport { escapeNamedColors, restoreNamedColors } from \"./named-colors.js\";\nimport { StyleSheet } from \"./stylesheet.js\";\n\n/** A `cx` argument: a non-empty class name, or a boolean or nullish value that is dropped. */\ntype CxToken = string | boolean | undefined | null;\n\n/** The `css`, `cx` and `injectGlobal` helpers bound to one GTK4 stylesheet and its cache of inserted styles. */\ntype Css = {\n /**\n * Serializes the given style interpolations, inserts the resulting rules into the stylesheet, and\n * returns the generated GTK4 CSS class name.\n */\n css: (...args: CSSInterpolation[]) => string;\n /**\n * Filters out falsy tokens and returns the remaining class names, collapsing two or more\n * GTKX-generated classes into one merged class where later styles win.\n */\n cx: (...classNames: CxToken[]) => string[];\n /** Serializes and inserts the given styles into the stylesheet globally, unscoped by any class. */\n injectGlobal: (...args: CSSInterpolation[]) => void;\n};\n\ntype TokenPartition = { rawClasses: string[]; registeredStyles: string[] };\ntype CssState = { sheet: StyleSheet; inserted: Set<string>; registered: RegisteredCache };\n\nconst KEY = \"gtkx\";\nconst LABEL_DECL_FIRST_CHAR = 108;\nconst LABEL_DECL_THIRD_CHAR = 98;\n\nconst removeLabel = (element: Element): void => {\n if (!(element.type === \"decl\" &&\n element.value.codePointAt(0) === LABEL_DECL_FIRST_CHAR &&\n element.value.codePointAt(2) === LABEL_DECL_THIRD_CHAR)) {\n return;\n }\n\n element.return = \"\";\n element.value = \"\";\n};\n\nconst getClassName = (serialized: SerializedStyles): string => `${KEY}-${serialized.name}`;\n\nconst terminateDeclarations = (styles: string): string => {\n const trimmed = styles.trimEnd();\n\n if (trimmed.length === 0 || trimmed.endsWith(\";\") || trimmed.endsWith(\"}\")) {\n return trimmed;\n }\n\n return `${trimmed};`;\n};\n\nconst isNonEmptyString = (token: CxToken): token is string => typeof token === \"string\" && token.length > 0;\n\nconst partitionTokens = (tokens: string[], registered: RegisteredCache): TokenPartition => {\n const rawClasses: string[] = [];\n const registeredStyles: string[] = [];\n\n for (const token of tokens) {\n const styles = registered[token];\n\n if (styles === undefined) {\n rawClasses.push(token);\n } else {\n registeredStyles.push(styles);\n }\n }\n\n return { rawClasses, registeredStyles };\n};\n\nconst runStylis = (sheet: StyleSheet, input: string): void => {\n stylisSerialize(\n compile(escapeNamedColors(input)),\n middleware([\n removeLabel,\n stringify,\n rulesheet((rule) => {\n sheet.insert(restoreNamedColors(rule));\n }),\n ]),\n );\n};\n\nconst didMarkNewStyle = (state: CssState, serialized: SerializedStyles): boolean => {\n if (state.inserted.has(serialized.name)) {\n return false;\n }\n\n state.inserted.add(serialized.name);\n\n return true;\n};\n\nconst insertStyles = (state: CssState, serialized: SerializedStyles): void => {\n if (!didMarkNewStyle(state, serialized)) {\n return;\n }\n\n const className = getClassName(serialized);\n const styles = terminateDeclarations(serialized.styles);\n runStylis(state.sheet, `.${className}{${styles}}`);\n state.registered[className] = styles;\n};\n\nconst insertWithoutScoping = (state: CssState, serialized: SerializedStyles): void => {\n if (!didMarkNewStyle(state, serialized)) {\n return;\n }\n\n runStylis(state.sheet, serialized.styles);\n};\n\nconst cssClassName = (state: CssState, args: CSSInterpolation[]): string => {\n const serialized = serializeStyles(args, state.registered);\n insertStyles(state, serialized);\n\n return getClassName(serialized);\n};\n\nconst cxClassNames = (state: CssState, classNames: CxToken[]): string[] => {\n const tokens = classNames.filter(isNonEmptyString);\n const { rawClasses, registeredStyles } = partitionTokens(tokens, state.registered);\n\n if (registeredStyles.length < 2) {\n return tokens;\n }\n\n return [...rawClasses, cssClassName(state, [registeredStyles.join(\"\")])];\n};\n\nconst createCss = (): Css => {\n const state: CssState = { sheet: new StyleSheet(), inserted: new Set(), registered: {} };\n\n return {\n css: (...args) => cssClassName(state, args),\n cx: (...classNames) => cxClassNames(state, classNames),\n injectGlobal: (...args) => {\n insertWithoutScoping(state, serializeStyles(args, state.registered));\n },\n };\n};\n\nexport { createCss, type Css };\n"]}
1
+ {"version":3,"file":"create-css.js","sourceRoot":"","sources":["../src/create-css.ts"],"names":[],"mappings":"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","sourcesContent":["import type { CSSInterpolation, RegisteredCache, SerializedStyles } from \"@emotion/serialize\";\nimport { serializeStyles } from \"@emotion/serialize\";\nimport { eachRule, terminateDeclarations } from \"./serialize-rule.js\";\nimport { StyleSheet } from \"./stylesheet.js\";\n\n/** A `cx` argument: a non-empty class name, or a boolean or nullish value that is dropped. */\ntype CxToken = string | boolean | undefined | null;\n\n/** The `css`, `cx` and `injectGlobal` helpers bound to one GTK4 stylesheet and its cache of inserted styles. */\ntype Css = {\n /**\n * Serializes the given style interpolations, inserts the resulting rules into the stylesheet, and\n * returns the generated GTK4 CSS class name.\n */\n css: (...args: CSSInterpolation[]) => string;\n /**\n * Filters out falsy tokens and returns the remaining class names, collapsing two or more\n * GTKX-generated classes into one merged class where later styles win.\n */\n cx: (...classNames: CxToken[]) => string[];\n /** Serializes and inserts the given styles into the stylesheet globally, unscoped by any class. */\n injectGlobal: (...args: CSSInterpolation[]) => void;\n};\n\ntype TokenPartition = { rawClasses: string[]; registeredStyles: string[] };\ntype CssState = { sheet: StyleSheet; inserted: Set<string>; registered: RegisteredCache };\n\nconst KEY = \"gtkx\";\n\nconst getClassName = (serialized: SerializedStyles): string => `${KEY}-${serialized.name}`;\nconst isNonEmptyString = (token: CxToken): token is string => typeof token === \"string\" && token.length > 0;\n\nconst partitionTokens = (tokens: string[], registered: RegisteredCache): TokenPartition => {\n const rawClasses: string[] = [];\n const registeredStyles: string[] = [];\n\n for (const token of tokens) {\n const styles = registered[token];\n\n if (styles === undefined) {\n rawClasses.push(token);\n } else {\n registeredStyles.push(styles);\n }\n }\n\n return { rawClasses, registeredStyles };\n};\n\nconst insertRules = (state: CssState, input: string): void => {\n eachRule(input, (rule) => {\n state.sheet.insert(rule);\n });\n};\n\nconst didMarkNewStyle = (state: CssState, serialized: SerializedStyles): boolean => {\n if (state.inserted.has(serialized.name)) {\n return false;\n }\n\n state.inserted.add(serialized.name);\n\n return true;\n};\n\nconst insertStyles = (state: CssState, serialized: SerializedStyles): void => {\n if (!didMarkNewStyle(state, serialized)) {\n return;\n }\n\n const className = getClassName(serialized);\n const styles = terminateDeclarations(serialized.styles);\n insertRules(state, `.${className}{${styles}}`);\n state.registered[className] = styles;\n};\n\nconst insertWithoutScoping = (state: CssState, serialized: SerializedStyles): void => {\n if (!didMarkNewStyle(state, serialized)) {\n return;\n }\n\n insertRules(state, serialized.styles);\n};\n\nconst cssClassName = (state: CssState, args: CSSInterpolation[]): string => {\n const serialized = serializeStyles(args, state.registered);\n insertStyles(state, serialized);\n\n return getClassName(serialized);\n};\n\nconst cxClassNames = (state: CssState, classNames: CxToken[]): string[] => {\n const tokens = classNames.filter(isNonEmptyString);\n const { rawClasses, registeredStyles } = partitionTokens(tokens, state.registered);\n\n if (registeredStyles.length < 2) {\n return tokens;\n }\n\n return [...rawClasses, cssClassName(state, [registeredStyles.join(\"\")])];\n};\n\nconst createCss = (): Css => {\n const state: CssState = { sheet: new StyleSheet(), inserted: new Set(), registered: {} };\n\n return {\n css: (...args) => cssClassName(state, args),\n cx: (...classNames) => cxClassNames(state, classNames),\n injectGlobal: (...args) => {\n insertWithoutScoping(state, serializeStyles(args, state.registered));\n },\n };\n};\n\nexport { createCss, type Css };\n"]}
@@ -0,0 +1,3 @@
1
+ export { attachParsingErrorLogger, type ProviderOptions, registerProviderForDefaultDisplay, } from "./provider.js";
2
+ export { scopedRule } from "./scoped-rule.js";
3
+ //# sourceMappingURL=internal.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"internal.d.ts","sourceRoot":"","sources":["../src/internal.ts"],"names":[],"mappings":"AAAA,OAAO,EACH,wBAAwB,EACxB,KAAK,eAAe,EACpB,iCAAiC,GACpC,MAAM,eAAe,CAAC;AACvB,OAAO,EAAE,UAAU,EAAE,MAAM,kBAAkB,CAAC"}
@@ -0,0 +1,3 @@
1
+ export { attachParsingErrorLogger, registerProviderForDefaultDisplay, } from "./provider.js";
2
+ export { scopedRule } from "./scoped-rule.js";
3
+ //# sourceMappingURL=internal.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"internal.js","sourceRoot":"","sources":["../src/internal.ts"],"names":[],"mappings":"AAAA,OAAO,EACH,wBAAwB,EAExB,iCAAiC,GACpC,MAAM,eAAe,CAAC;AACvB,OAAO,EAAE,UAAU,EAAE,MAAM,kBAAkB,CAAC","sourcesContent":["export {\n attachParsingErrorLogger,\n type ProviderOptions,\n registerProviderForDefaultDisplay,\n} from \"./provider.js\";\nexport { scopedRule } from \"./scoped-rule.js\";\n"]}
@@ -1,6 +1,10 @@
1
1
  import type { Logger } from "@gtkx/utils";
2
2
  import { CssProvider } from "@gtkx/gi/gtk";
3
- declare const registerProviderForDefaultDisplay: (priority?: number) => CssProvider;
3
+ type ProviderOptions = {
4
+ priority: number;
5
+ followsPreferences: boolean;
6
+ };
7
+ declare const registerProviderForDefaultDisplay: (options?: Partial<ProviderOptions>) => CssProvider;
4
8
  declare const attachParsingErrorLogger: (provider: CssProvider, log: Logger, subject: string) => void;
5
- export { registerProviderForDefaultDisplay, attachParsingErrorLogger };
9
+ export { type ProviderOptions, registerProviderForDefaultDisplay, attachParsingErrorLogger };
6
10
  //# sourceMappingURL=provider.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"provider.d.ts","sourceRoot":"","sources":["../src/provider.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,MAAM,EAAE,MAAM,aAAa,CAAC;AAE1C,OAAO,EAAE,WAAW,EAAqD,MAAM,cAAc,CAAC;AAE9F,QAAA,MAAM,iCAAiC,GACnC,WAAU,MAA4C,KACvD,WAoBF,CAAC;AAEF,QAAA,MAAM,wBAAwB,GAAI,UAAU,WAAW,EAAE,KAAK,MAAM,EAAE,SAAS,MAAM,KAAG,IAMvF,CAAC;AAEF,OAAO,EAAE,iCAAiC,EAAE,wBAAwB,EAAE,CAAC"}
1
+ {"version":3,"file":"provider.d.ts","sourceRoot":"","sources":["../src/provider.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,MAAM,EAAE,MAAM,aAAa,CAAC;AAE1C,OAAO,EAEH,WAAW,EAId,MAAM,cAAc,CAAC;AAEtB,KAAK,eAAe,GAAG;IAAE,QAAQ,EAAE,MAAM,CAAC;IAAC,kBAAkB,EAAE,OAAO,CAAA;CAAE,CAAC;AAoCzE,QAAA,MAAM,iCAAiC,GAAI,UAAU,OAAO,CAAC,eAAe,CAAC,KAAG,WAyB/E,CAAC;AAEF,QAAA,MAAM,wBAAwB,GAAI,UAAU,WAAW,EAAE,KAAK,MAAM,EAAE,SAAS,MAAM,KAAG,IAMvF,CAAC;AAEF,OAAO,EAAE,KAAK,eAAe,EAAE,iCAAiC,EAAE,wBAAwB,EAAE,CAAC"}
package/dist/provider.js CHANGED
@@ -1,11 +1,40 @@
1
1
  import { DisplayManager } from "@gtkx/gi/gdk";
2
- import { CssProvider, STYLE_PROVIDER_PRIORITY_APPLICATION, StyleContext } from "@gtkx/gi/gtk";
3
- const registerProviderForDefaultDisplay = (priority = STYLE_PROVIDER_PRIORITY_APPLICATION) => {
2
+ import { checkVersion, CssProvider, Settings, STYLE_PROVIDER_PRIORITY_APPLICATION, StyleContext, } from "@gtkx/gi/gtk";
3
+ const REDUCED_MOTION_MINOR = 22;
4
+ const DEFAULT_OPTIONS = {
5
+ priority: STYLE_PROVIDER_PRIORITY_APPLICATION,
6
+ followsPreferences: true,
7
+ };
8
+ const hasReducedMotion = () => checkVersion(4, REDUCED_MOTION_MINOR, 0) === null;
9
+ const applyPreferences = (provider, settings) => {
10
+ provider.prefersColorScheme = settings.gtkInterfaceColorScheme;
11
+ provider.prefersContrast = settings.gtkInterfaceContrast;
12
+ if (hasReducedMotion()) {
13
+ provider.prefersReducedMotion = settings.gtkInterfaceReducedMotion;
14
+ }
15
+ };
16
+ const followSystemPreferences = (provider, display) => {
17
+ const settings = Settings.getForDisplay(display);
18
+ const sync = () => {
19
+ applyPreferences(provider, settings);
20
+ };
21
+ sync();
22
+ settings.on("notify::gtk-interface-color-scheme", sync);
23
+ settings.on("notify::gtk-interface-contrast", sync);
24
+ if (hasReducedMotion()) {
25
+ settings.on("notify::gtk-interface-reduced-motion", sync);
26
+ }
27
+ };
28
+ const registerProviderForDefaultDisplay = (options) => {
29
+ const { priority, followsPreferences } = { ...DEFAULT_OPTIONS, ...options };
4
30
  const provider = new CssProvider();
5
31
  const manager = DisplayManager.get();
6
32
  const attach = (display) => {
7
33
  // eslint-disable-next-line @typescript-eslint/no-deprecated
8
34
  StyleContext.addProviderForDisplay(display, provider, priority);
35
+ if (followsPreferences) {
36
+ followSystemPreferences(provider, display);
37
+ }
9
38
  };
10
39
  const initialDisplay = manager.getDefaultDisplay();
11
40
  if (initialDisplay) {
@@ -1 +1 @@
1
- {"version":3,"file":"provider.js","sourceRoot":"","sources":["../src/provider.ts"],"names":[],"mappings":"AACA,OAAO,EAAgB,cAAc,EAAE,MAAM,cAAc,CAAC;AAC5D,OAAO,EAAE,WAAW,EAAE,mCAAmC,EAAE,YAAY,EAAE,MAAM,cAAc,CAAC;AAE9F,MAAM,iCAAiC,GAAG,CACtC,WAAmB,mCAAmC,EAC3C,EAAE;IACb,MAAM,QAAQ,GAAG,IAAI,WAAW,EAAE,CAAC;IACnC,MAAM,OAAO,GAAG,cAAc,CAAC,GAAG,EAAE,CAAC;IAErC,MAAM,MAAM,GAAG,CAAC,OAAgB,EAAQ,EAAE;QACtC,4DAA4D;QAC5D,YAAY,CAAC,qBAAqB,CAAC,OAAO,EAAE,QAAQ,EAAE,QAAQ,CAAC,CAAC;IACpE,CAAC,CAAC;IAEF,MAAM,cAAc,GAAG,OAAO,CAAC,iBAAiB,EAAE,CAAC;IAEnD,IAAI,cAAc,EAAE,CAAC;QACjB,MAAM,CAAC,cAAc,CAAC,CAAC;IAC3B,CAAC;SAAM,CAAC;QACJ,OAAO,CAAC,IAAI,CAAC,gBAAgB,EAAE,CAAC,aAAsB,EAAQ,EAAE;YAC5D,MAAM,CAAC,aAAa,CAAC,CAAC;QAC1B,CAAC,CAAC,CAAC;IACP,CAAC;IAED,OAAO,QAAQ,CAAC;AACpB,CAAC,CAAC;AAEF,MAAM,wBAAwB,GAAG,CAAC,QAAqB,EAAE,GAAW,EAAE,OAAe,EAAQ,EAAE;IAC3F,IAAI,OAAO,CAAC,GAAG,CAAC,QAAQ,KAAK,YAAY,EAAE,CAAC;QACxC,QAAQ,CAAC,EAAE,CAAC,eAAe,EAAE,CAAC,OAAO,EAAE,KAAK,EAAE,EAAE;YAC5C,GAAG,CAAC,IAAI,CAAC,iBAAiB,OAAO,OAAO,OAAO,CAAC,QAAQ,EAAE,KAAK,KAAK,CAAC,OAAO,EAAE,CAAC,CAAC;QACpF,CAAC,CAAC,CAAC;IACP,CAAC;AACL,CAAC,CAAC;AAEF,OAAO,EAAE,iCAAiC,EAAE,wBAAwB,EAAE,CAAC","sourcesContent":["import type { Logger } from \"@gtkx/utils\";\nimport { type Display, DisplayManager } from \"@gtkx/gi/gdk\";\nimport { CssProvider, STYLE_PROVIDER_PRIORITY_APPLICATION, StyleContext } from \"@gtkx/gi/gtk\";\n\nconst registerProviderForDefaultDisplay = (\n priority: number = STYLE_PROVIDER_PRIORITY_APPLICATION,\n): CssProvider => {\n const provider = new CssProvider();\n const manager = DisplayManager.get();\n\n const attach = (display: Display): void => {\n // eslint-disable-next-line @typescript-eslint/no-deprecated\n StyleContext.addProviderForDisplay(display, provider, priority);\n };\n\n const initialDisplay = manager.getDefaultDisplay();\n\n if (initialDisplay) {\n attach(initialDisplay);\n } else {\n manager.once(\"display-opened\", (openedDisplay: Display): void => {\n attach(openedDisplay);\n });\n }\n\n return provider;\n};\n\nconst attachParsingErrorLogger = (provider: CssProvider, log: Logger, subject: string): void => {\n if (process.env.NODE_ENV !== \"production\") {\n provider.on(\"parsing-error\", (section, error) => {\n log.warn(`GTK4 rejected ${subject} at ${section.toString()}: ${error.message}`);\n });\n }\n};\n\nexport { registerProviderForDefaultDisplay, attachParsingErrorLogger };\n"]}
1
+ {"version":3,"file":"provider.js","sourceRoot":"","sources":["../src/provider.ts"],"names":[],"mappings":"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","sourcesContent":["import type { Logger } from \"@gtkx/utils\";\nimport { type Display, DisplayManager } from \"@gtkx/gi/gdk\";\nimport {\n checkVersion,\n CssProvider,\n Settings,\n STYLE_PROVIDER_PRIORITY_APPLICATION,\n StyleContext,\n} from \"@gtkx/gi/gtk\";\n\ntype ProviderOptions = { priority: number; followsPreferences: boolean };\n\nconst REDUCED_MOTION_MINOR = 22;\n\nconst DEFAULT_OPTIONS: ProviderOptions = {\n priority: STYLE_PROVIDER_PRIORITY_APPLICATION,\n followsPreferences: true,\n};\n\nconst hasReducedMotion = (): boolean => checkVersion(4, REDUCED_MOTION_MINOR, 0) === null;\n\nconst applyPreferences = (provider: CssProvider, settings: Settings): void => {\n provider.prefersColorScheme = settings.gtkInterfaceColorScheme;\n provider.prefersContrast = settings.gtkInterfaceContrast;\n\n if (hasReducedMotion()) {\n provider.prefersReducedMotion = settings.gtkInterfaceReducedMotion;\n }\n};\n\nconst followSystemPreferences = (provider: CssProvider, display: Display): void => {\n const settings = Settings.getForDisplay(display);\n\n const sync = (): void => {\n applyPreferences(provider, settings);\n };\n\n sync();\n settings.on(\"notify::gtk-interface-color-scheme\", sync);\n settings.on(\"notify::gtk-interface-contrast\", sync);\n\n if (hasReducedMotion()) {\n settings.on(\"notify::gtk-interface-reduced-motion\", sync);\n }\n};\n\nconst registerProviderForDefaultDisplay = (options?: Partial<ProviderOptions>): CssProvider => {\n const { priority, followsPreferences } = { ...DEFAULT_OPTIONS, ...options };\n const provider = new CssProvider();\n const manager = DisplayManager.get();\n\n const attach = (display: Display): void => {\n // eslint-disable-next-line @typescript-eslint/no-deprecated\n StyleContext.addProviderForDisplay(display, provider, priority);\n\n if (followsPreferences) {\n followSystemPreferences(provider, display);\n }\n };\n\n const initialDisplay = manager.getDefaultDisplay();\n\n if (initialDisplay) {\n attach(initialDisplay);\n } else {\n manager.once(\"display-opened\", (openedDisplay: Display): void => {\n attach(openedDisplay);\n });\n }\n\n return provider;\n};\n\nconst attachParsingErrorLogger = (provider: CssProvider, log: Logger, subject: string): void => {\n if (process.env.NODE_ENV !== \"production\") {\n provider.on(\"parsing-error\", (section, error) => {\n log.warn(`GTK4 rejected ${subject} at ${section.toString()}: ${error.message}`);\n });\n }\n};\n\nexport { type ProviderOptions, registerProviderForDefaultDisplay, attachParsingErrorLogger };\n"]}
@@ -0,0 +1,5 @@
1
+ declare const NUL_REASON = "carries a NUL byte, which GTK4 cannot load";
2
+ declare const hasNul: (rule: string) => boolean;
3
+ declare const printableRule: (rule: string) => string;
4
+ export { hasNul, NUL_REASON, printableRule };
5
+ //# sourceMappingURL=rule-text.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"rule-text.d.ts","sourceRoot":"","sources":["../src/rule-text.ts"],"names":[],"mappings":"AACA,QAAA,MAAM,UAAU,+CAA+C,CAAC;AAEhE,QAAA,MAAM,MAAM,GAAI,MAAM,MAAM,KAAG,OAA6B,CAAC;AAC7D,QAAA,MAAM,aAAa,GAAI,MAAM,MAAM,KAAG,MAA8C,CAAC;AAErF,OAAO,EAAE,MAAM,EAAE,UAAU,EAAE,aAAa,EAAE,CAAC"}
@@ -0,0 +1,6 @@
1
+ const NUL = "\u{0}";
2
+ const NUL_REASON = "carries a NUL byte, which GTK4 cannot load";
3
+ const hasNul = (rule) => rule.includes(NUL);
4
+ const printableRule = (rule) => rule.replaceAll(NUL, String.raw `\0`);
5
+ export { hasNul, NUL_REASON, printableRule };
6
+ //# sourceMappingURL=rule-text.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"rule-text.js","sourceRoot":"","sources":["../src/rule-text.ts"],"names":[],"mappings":"AAAA,MAAM,GAAG,GAAG,OAAO,CAAC;AACpB,MAAM,UAAU,GAAG,4CAA4C,CAAC;AAEhE,MAAM,MAAM,GAAG,CAAC,IAAY,EAAW,EAAE,CAAC,IAAI,CAAC,QAAQ,CAAC,GAAG,CAAC,CAAC;AAC7D,MAAM,aAAa,GAAG,CAAC,IAAY,EAAU,EAAE,CAAC,IAAI,CAAC,UAAU,CAAC,GAAG,EAAE,MAAM,CAAC,GAAG,CAAA,IAAI,CAAC,CAAC;AAErF,OAAO,EAAE,MAAM,EAAE,UAAU,EAAE,aAAa,EAAE,CAAC","sourcesContent":["const NUL = \"\\u{0}\";\nconst NUL_REASON = \"carries a NUL byte, which GTK4 cannot load\";\n\nconst hasNul = (rule: string): boolean => rule.includes(NUL);\nconst printableRule = (rule: string): string => rule.replaceAll(NUL, String.raw`\\0`);\n\nexport { hasNul, NUL_REASON, printableRule };\n"]}
@@ -0,0 +1,3 @@
1
+ declare const scopedRule: (className: string, style: object) => string;
2
+ export { scopedRule };
3
+ //# sourceMappingURL=scoped-rule.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"scoped-rule.d.ts","sourceRoot":"","sources":["../src/scoped-rule.ts"],"names":[],"mappings":"AA4CA,QAAA,MAAM,UAAU,GAAI,WAAW,MAAM,EAAE,OAAO,MAAM,KAAG,MActD,CAAC;AAEF,OAAO,EAAE,UAAU,EAAE,CAAC"}
@@ -0,0 +1,42 @@
1
+ import { serializeStyles } from "@emotion/serialize";
2
+ import { createLogger } from "@gtkx/utils";
3
+ import { hasNul, NUL_REASON, printableRule } from "./rule-text.js";
4
+ import { eachRule, terminateDeclarations } from "./serialize-rule.js";
5
+ const log = createLogger("css");
6
+ const ESCAPE_REASON = "does not start from the widget it belongs to";
7
+ const NAME_CHAR = /[\w-]/;
8
+ const isScoped = (rule, className) => {
9
+ const prefix = `.${className}`;
10
+ if (!rule.startsWith(prefix)) {
11
+ return false;
12
+ }
13
+ const next = rule.charAt(prefix.length);
14
+ return next !== "" && !NAME_CHAR.test(next);
15
+ };
16
+ const unusableReason = (rule, className) => {
17
+ if (hasNul(rule)) {
18
+ return NUL_REASON;
19
+ }
20
+ return isScoped(rule, className) ? null : ESCAPE_REASON;
21
+ };
22
+ const collect = (scope, rule) => {
23
+ const reason = unusableReason(rule, scope.className);
24
+ if (reason !== null) {
25
+ log.warn(`Dropped a style rule that ${reason}: ${printableRule(rule)}`);
26
+ return;
27
+ }
28
+ scope.rules.push(rule);
29
+ };
30
+ const scopedRule = (className, style) => {
31
+ const declarations = terminateDeclarations(serializeStyles([style], {}).styles);
32
+ if (declarations.length === 0) {
33
+ return "";
34
+ }
35
+ const scope = { className, rules: [] };
36
+ eachRule(`.${className}{${declarations}}`, (rule) => {
37
+ collect(scope, rule);
38
+ });
39
+ return scope.rules.join("");
40
+ };
41
+ export { scopedRule };
42
+ //# sourceMappingURL=scoped-rule.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"scoped-rule.js","sourceRoot":"","sources":["../src/scoped-rule.ts"],"names":[],"mappings":"AACA,OAAO,EAAE,eAAe,EAAE,MAAM,oBAAoB,CAAC;AACrD,OAAO,EAAE,YAAY,EAAE,MAAM,aAAa,CAAC;AAC3C,OAAO,EAAE,MAAM,EAAE,UAAU,EAAE,aAAa,EAAE,MAAM,gBAAgB,CAAC;AACnE,OAAO,EAAE,QAAQ,EAAE,qBAAqB,EAAE,MAAM,qBAAqB,CAAC;AAItE,MAAM,GAAG,GAAG,YAAY,CAAC,KAAK,CAAC,CAAC;AAChC,MAAM,aAAa,GAAG,8CAA8C,CAAC;AACrE,MAAM,SAAS,GAAG,OAAO,CAAC;AAE1B,MAAM,QAAQ,GAAG,CAAC,IAAY,EAAE,SAAiB,EAAW,EAAE;IAC1D,MAAM,MAAM,GAAG,IAAI,SAAS,EAAE,CAAC;IAE/B,IAAI,CAAC,IAAI,CAAC,UAAU,CAAC,MAAM,CAAC,EAAE,CAAC;QAC3B,OAAO,KAAK,CAAC;IACjB,CAAC;IAED,MAAM,IAAI,GAAG,IAAI,CAAC,MAAM,CAAC,MAAM,CAAC,MAAM,CAAC,CAAC;IAExC,OAAO,IAAI,KAAK,EAAE,IAAI,CAAC,SAAS,CAAC,IAAI,CAAC,IAAI,CAAC,CAAC;AAChD,CAAC,CAAC;AAEF,MAAM,cAAc,GAAG,CAAC,IAAY,EAAE,SAAiB,EAAiB,EAAE;IACtE,IAAI,MAAM,CAAC,IAAI,CAAC,EAAE,CAAC;QACf,OAAO,UAAU,CAAC;IACtB,CAAC;IAED,OAAO,QAAQ,CAAC,IAAI,EAAE,SAAS,CAAC,CAAC,CAAC,CAAC,IAAI,CAAC,CAAC,CAAC,aAAa,CAAC;AAC5D,CAAC,CAAC;AAEF,MAAM,OAAO,GAAG,CAAC,KAAY,EAAE,IAAY,EAAQ,EAAE;IACjD,MAAM,MAAM,GAAG,cAAc,CAAC,IAAI,EAAE,KAAK,CAAC,SAAS,CAAC,CAAC;IAErD,IAAI,MAAM,KAAK,IAAI,EAAE,CAAC;QAClB,GAAG,CAAC,IAAI,CAAC,6BAA6B,MAAM,KAAK,aAAa,CAAC,IAAI,CAAC,EAAE,CAAC,CAAC;QAExE,OAAO;IACX,CAAC;IAED,KAAK,CAAC,KAAK,CAAC,IAAI,CAAC,IAAI,CAAC,CAAC;AAC3B,CAAC,CAAC;AAEF,MAAM,UAAU,GAAG,CAAC,SAAiB,EAAE,KAAa,EAAU,EAAE;IAC5D,MAAM,YAAY,GAAG,qBAAqB,CAAC,eAAe,CAAC,CAAC,KAAkB,CAAC,EAAE,EAAE,CAAC,CAAC,MAAM,CAAC,CAAC;IAE7F,IAAI,YAAY,CAAC,MAAM,KAAK,CAAC,EAAE,CAAC;QAC5B,OAAO,EAAE,CAAC;IACd,CAAC;IAED,MAAM,KAAK,GAAU,EAAE,SAAS,EAAE,KAAK,EAAE,EAAE,EAAE,CAAC;IAE9C,QAAQ,CAAC,IAAI,SAAS,IAAI,YAAY,GAAG,EAAE,CAAC,IAAI,EAAE,EAAE;QAChD,OAAO,CAAC,KAAK,EAAE,IAAI,CAAC,CAAC;IACzB,CAAC,CAAC,CAAC;IAEH,OAAO,KAAK,CAAC,KAAK,CAAC,IAAI,CAAC,EAAE,CAAC,CAAC;AAChC,CAAC,CAAC;AAEF,OAAO,EAAE,UAAU,EAAE,CAAC","sourcesContent":["import type { CSSObject } from \"@emotion/serialize\";\nimport { serializeStyles } from \"@emotion/serialize\";\nimport { createLogger } from \"@gtkx/utils\";\nimport { hasNul, NUL_REASON, printableRule } from \"./rule-text.js\";\nimport { eachRule, terminateDeclarations } from \"./serialize-rule.js\";\n\ntype Scope = { className: string; rules: string[] };\n\nconst log = createLogger(\"css\");\nconst ESCAPE_REASON = \"does not start from the widget it belongs to\";\nconst NAME_CHAR = /[\\w-]/;\n\nconst isScoped = (rule: string, className: string): boolean => {\n const prefix = `.${className}`;\n\n if (!rule.startsWith(prefix)) {\n return false;\n }\n\n const next = rule.charAt(prefix.length);\n\n return next !== \"\" && !NAME_CHAR.test(next);\n};\n\nconst unusableReason = (rule: string, className: string): string | null => {\n if (hasNul(rule)) {\n return NUL_REASON;\n }\n\n return isScoped(rule, className) ? null : ESCAPE_REASON;\n};\n\nconst collect = (scope: Scope, rule: string): void => {\n const reason = unusableReason(rule, scope.className);\n\n if (reason !== null) {\n log.warn(`Dropped a style rule that ${reason}: ${printableRule(rule)}`);\n\n return;\n }\n\n scope.rules.push(rule);\n};\n\nconst scopedRule = (className: string, style: object): string => {\n const declarations = terminateDeclarations(serializeStyles([style as CSSObject], {}).styles);\n\n if (declarations.length === 0) {\n return \"\";\n }\n\n const scope: Scope = { className, rules: [] };\n\n eachRule(`.${className}{${declarations}}`, (rule) => {\n collect(scope, rule);\n });\n\n return scope.rules.join(\"\");\n};\n\nexport { scopedRule };\n"]}
@@ -0,0 +1,4 @@
1
+ declare const terminateDeclarations: (styles: string) => string;
2
+ declare const eachRule: (input: string, visit: (rule: string) => void) => void;
3
+ export { eachRule, terminateDeclarations };
4
+ //# sourceMappingURL=serialize-rule.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"serialize-rule.d.ts","sourceRoot":"","sources":["../src/serialize-rule.ts"],"names":[],"mappings":"AAkBA,QAAA,MAAM,qBAAqB,GAAI,QAAQ,MAAM,KAAG,MAQ/C,CAAC;AAEF,QAAA,MAAM,QAAQ,GAAI,OAAO,MAAM,EAAE,OAAO,CAAC,IAAI,EAAE,MAAM,KAAK,IAAI,KAAG,IAWhE,CAAC;AAEF,OAAO,EAAE,QAAQ,EAAE,qBAAqB,EAAE,CAAC"}
@@ -0,0 +1,31 @@
1
+ import { compile, middleware, rulesheet, stringify, serialize as stylisSerialize } from "stylis";
2
+ import { escapeNamedColors, restoreNamedColors } from "./named-colors.js";
3
+ const LABEL_DECL_FIRST_CHAR = 108;
4
+ const LABEL_DECL_THIRD_CHAR = 98;
5
+ const removeLabel = (element) => {
6
+ if (!(element.type === "decl" &&
7
+ element.value.codePointAt(0) === LABEL_DECL_FIRST_CHAR &&
8
+ element.value.codePointAt(2) === LABEL_DECL_THIRD_CHAR)) {
9
+ return;
10
+ }
11
+ element.return = "";
12
+ element.value = "";
13
+ };
14
+ const terminateDeclarations = (styles) => {
15
+ const trimmed = styles.trimEnd();
16
+ if (trimmed.length === 0 || trimmed.endsWith(";") || trimmed.endsWith("}")) {
17
+ return trimmed;
18
+ }
19
+ return `${trimmed};`;
20
+ };
21
+ const eachRule = (input, visit) => {
22
+ stylisSerialize(compile(escapeNamedColors(input)), middleware([
23
+ removeLabel,
24
+ stringify,
25
+ rulesheet((rule) => {
26
+ visit(restoreNamedColors(rule));
27
+ }),
28
+ ]));
29
+ };
30
+ export { eachRule, terminateDeclarations };
31
+ //# sourceMappingURL=serialize-rule.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"serialize-rule.js","sourceRoot":"","sources":["../src/serialize-rule.ts"],"names":[],"mappings":"AACA,OAAO,EAAE,OAAO,EAAE,UAAU,EAAE,SAAS,EAAE,SAAS,EAAE,SAAS,IAAI,eAAe,EAAE,MAAM,QAAQ,CAAC;AACjG,OAAO,EAAE,iBAAiB,EAAE,kBAAkB,EAAE,MAAM,mBAAmB,CAAC;AAE1E,MAAM,qBAAqB,GAAG,GAAG,CAAC;AAClC,MAAM,qBAAqB,GAAG,EAAE,CAAC;AAEjC,MAAM,WAAW,GAAG,CAAC,OAAgB,EAAQ,EAAE;IAC3C,IAAI,CAAC,CAAC,OAAO,CAAC,IAAI,KAAK,MAAM;QACzB,OAAO,CAAC,KAAK,CAAC,WAAW,CAAC,CAAC,CAAC,KAAK,qBAAqB;QACtD,OAAO,CAAC,KAAK,CAAC,WAAW,CAAC,CAAC,CAAC,KAAK,qBAAqB,CAAC,EAAE,CAAC;QAC1D,OAAO;IACX,CAAC;IAED,OAAO,CAAC,MAAM,GAAG,EAAE,CAAC;IACpB,OAAO,CAAC,KAAK,GAAG,EAAE,CAAC;AACvB,CAAC,CAAC;AAEF,MAAM,qBAAqB,GAAG,CAAC,MAAc,EAAU,EAAE;IACrD,MAAM,OAAO,GAAG,MAAM,CAAC,OAAO,EAAE,CAAC;IAEjC,IAAI,OAAO,CAAC,MAAM,KAAK,CAAC,IAAI,OAAO,CAAC,QAAQ,CAAC,GAAG,CAAC,IAAI,OAAO,CAAC,QAAQ,CAAC,GAAG,CAAC,EAAE,CAAC;QACzE,OAAO,OAAO,CAAC;IACnB,CAAC;IAED,OAAO,GAAG,OAAO,GAAG,CAAC;AACzB,CAAC,CAAC;AAEF,MAAM,QAAQ,GAAG,CAAC,KAAa,EAAE,KAA6B,EAAQ,EAAE;IACpE,eAAe,CACX,OAAO,CAAC,iBAAiB,CAAC,KAAK,CAAC,CAAC,EACjC,UAAU,CAAC;QACP,WAAW;QACX,SAAS;QACT,SAAS,CAAC,CAAC,IAAI,EAAE,EAAE;YACf,KAAK,CAAC,kBAAkB,CAAC,IAAI,CAAC,CAAC,CAAC;QACpC,CAAC,CAAC;KACL,CAAC,CACL,CAAC;AACN,CAAC,CAAC;AAEF,OAAO,EAAE,QAAQ,EAAE,qBAAqB,EAAE,CAAC","sourcesContent":["import type { Element } from \"stylis\";\nimport { compile, middleware, rulesheet, stringify, serialize as stylisSerialize } from \"stylis\";\nimport { escapeNamedColors, restoreNamedColors } from \"./named-colors.js\";\n\nconst LABEL_DECL_FIRST_CHAR = 108;\nconst LABEL_DECL_THIRD_CHAR = 98;\n\nconst removeLabel = (element: Element): void => {\n if (!(element.type === \"decl\" &&\n element.value.codePointAt(0) === LABEL_DECL_FIRST_CHAR &&\n element.value.codePointAt(2) === LABEL_DECL_THIRD_CHAR)) {\n return;\n }\n\n element.return = \"\";\n element.value = \"\";\n};\n\nconst terminateDeclarations = (styles: string): string => {\n const trimmed = styles.trimEnd();\n\n if (trimmed.length === 0 || trimmed.endsWith(\";\") || trimmed.endsWith(\"}\")) {\n return trimmed;\n }\n\n return `${trimmed};`;\n};\n\nconst eachRule = (input: string, visit: (rule: string) => void): void => {\n stylisSerialize(\n compile(escapeNamedColors(input)),\n middleware([\n removeLabel,\n stringify,\n rulesheet((rule) => {\n visit(restoreNamedColors(rule));\n }),\n ]),\n );\n};\n\nexport { eachRule, terminateDeclarations };\n"]}
@@ -1 +1 @@
1
- {"version":3,"file":"stylesheet.d.ts","sourceRoot":"","sources":["../src/stylesheet.ts"],"names":[],"mappings":"AAcA,cAAM,UAAU;IACZ,OAAO,CAAC,GAAG,CAAM;IACjB,OAAO,CAAC,QAAQ,CAAgC;IAChD,OAAO,CAAC,eAAe,CAAS;IAEhC,OAAO,CAAC,cAAc;IAYtB,OAAO,CAAC,cAAc;IAatB,MAAM,CAAC,IAAI,EAAE,MAAM,GAAG,IAAI;CAY7B;AAED,OAAO,EAAE,UAAU,EAAE,CAAC"}
1
+ {"version":3,"file":"stylesheet.d.ts","sourceRoot":"","sources":["../src/stylesheet.ts"],"names":[],"mappings":"AAUA,cAAM,UAAU;IACZ,OAAO,CAAC,GAAG,CAAM;IACjB,OAAO,CAAC,QAAQ,CAAgC;IAChD,OAAO,CAAC,eAAe,CAAS;IAEhC,OAAO,CAAC,cAAc;IAYtB,OAAO,CAAC,cAAc;IAatB,MAAM,CAAC,IAAI,EAAE,MAAM,GAAG,IAAI;CAY7B;AAED,OAAO,EAAE,UAAU,EAAE,CAAC"}
@@ -1,11 +1,9 @@
1
1
  import { createLogger } from "@gtkx/utils";
2
2
  import { attachParsingErrorLogger, registerProviderForDefaultDisplay } from "./provider.js";
3
+ import { hasNul, NUL_REASON, printableRule } from "./rule-text.js";
3
4
  import { containmentFailure } from "./self-contained.js";
4
5
  const log = createLogger("css");
5
- const NUL = "\u{0}";
6
- const NUL_REASON = "carries a NUL byte, which GTK4 cannot load";
7
- const unusableReason = (rule) => rule.includes(NUL) ? NUL_REASON : containmentFailure(rule);
8
- const printableRule = (rule) => rule.replaceAll(NUL, String.raw `\0`);
6
+ const unusableReason = (rule) => (hasNul(rule) ? NUL_REASON : containmentFailure(rule));
9
7
  class StyleSheet {
10
8
  css = "";
11
9
  provider = null;
@@ -1 +1 @@
1
- {"version":3,"file":"stylesheet.js","sourceRoot":"","sources":["../src/stylesheet.ts"],"names":[],"mappings":"AACA,OAAO,EAAE,YAAY,EAAE,MAAM,aAAa,CAAC;AAC3C,OAAO,EAAE,wBAAwB,EAAE,iCAAiC,EAAE,MAAM,eAAe,CAAC;AAC5F,OAAO,EAAE,kBAAkB,EAAE,MAAM,qBAAqB,CAAC;AAEzD,MAAM,GAAG,GAAG,YAAY,CAAC,KAAK,CAAC,CAAC;AAChC,MAAM,GAAG,GAAG,OAAO,CAAC;AACpB,MAAM,UAAU,GAAG,4CAA4C,CAAC;AAEhE,MAAM,cAAc,GAAG,CAAC,IAAY,EAAiB,EAAE,CACnD,IAAI,CAAC,QAAQ,CAAC,GAAG,CAAC,CAAC,CAAC,CAAC,UAAU,CAAC,CAAC,CAAC,kBAAkB,CAAC,IAAI,CAAC,CAAC;AAE/D,MAAM,aAAa,GAAG,CAAC,IAAY,EAAU,EAAE,CAAC,IAAI,CAAC,UAAU,CAAC,GAAG,EAAE,MAAM,CAAC,GAAG,CAAA,IAAI,CAAC,CAAC;AAErF,MAAM,UAAU;IACJ,GAAG,GAAG,EAAE,CAAC;IACT,QAAQ,GAA2B,IAAI,CAAC;IACxC,eAAe,GAAG,KAAK,CAAC;IAExB,cAAc;QAClB,IAAI,IAAI,CAAC,QAAQ,EAAE,CAAC;YAChB,OAAO,IAAI,CAAC,QAAQ,CAAC;QACzB,CAAC;QAED,MAAM,QAAQ,GAAG,iCAAiC,EAAE,CAAC;QACrD,IAAI,CAAC,QAAQ,GAAG,QAAQ,CAAC;QACzB,wBAAwB,CAAC,QAAQ,EAAE,GAAG,EAAE,KAAK,CAAC,CAAC;QAE/C,OAAO,QAAQ,CAAC;IACpB,CAAC;IAEO,cAAc;QAClB,IAAI,IAAI,CAAC,eAAe,EAAE,CAAC;YACvB,OAAO;QACX,CAAC;QAED,IAAI,CAAC,eAAe,GAAG,IAAI,CAAC;QAE5B,cAAc,CAAC,GAAG,EAAE;YAChB,IAAI,CAAC,eAAe,GAAG,KAAK,CAAC;YAC7B,IAAI,CAAC,cAAc,EAAE,CAAC,cAAc,CAAC,IAAI,CAAC,GAAG,CAAC,CAAC;QACnD,CAAC,CAAC,CAAC;IACP,CAAC;IAED,MAAM,CAAC,IAAY;QACf,MAAM,MAAM,GAAG,cAAc,CAAC,IAAI,CAAC,CAAC;QAEpC,IAAI,MAAM,KAAK,IAAI,EAAE,CAAC;YAClB,GAAG,CAAC,IAAI,CAAC,qCAAqC,MAAM,KAAK,aAAa,CAAC,IAAI,CAAC,EAAE,CAAC,CAAC;YAEhF,OAAO;QACX,CAAC;QAED,IAAI,CAAC,GAAG,IAAI,IAAI,CAAC,GAAG,CAAC,MAAM,GAAG,CAAC,CAAC,CAAC,CAAC,KAAK,IAAI,EAAE,CAAC,CAAC,CAAC,IAAI,CAAC;QACrD,IAAI,CAAC,cAAc,EAAE,CAAC;IAC1B,CAAC;CACJ;AAED,OAAO,EAAE,UAAU,EAAE,CAAC","sourcesContent":["import type * as Gtk from \"@gtkx/gi/gtk\";\nimport { createLogger } from \"@gtkx/utils\";\nimport { attachParsingErrorLogger, registerProviderForDefaultDisplay } from \"./provider.js\";\nimport { containmentFailure } from \"./self-contained.js\";\n\nconst log = createLogger(\"css\");\nconst NUL = \"\\u{0}\";\nconst NUL_REASON = \"carries a NUL byte, which GTK4 cannot load\";\n\nconst unusableReason = (rule: string): string | null =>\n rule.includes(NUL) ? NUL_REASON : containmentFailure(rule);\n\nconst printableRule = (rule: string): string => rule.replaceAll(NUL, String.raw`\\0`);\n\nclass StyleSheet {\n private css = \"\";\n private provider: Gtk.CssProvider | null = null;\n private updateScheduled = false;\n\n private ensureProvider(): Gtk.CssProvider {\n if (this.provider) {\n return this.provider;\n }\n\n const provider = registerProviderForDefaultDisplay();\n this.provider = provider;\n attachParsingErrorLogger(provider, log, \"CSS\");\n\n return provider;\n }\n\n private scheduleUpdate(): void {\n if (this.updateScheduled) {\n return;\n }\n\n this.updateScheduled = true;\n\n queueMicrotask(() => {\n this.updateScheduled = false;\n this.ensureProvider().loadFromString(this.css);\n });\n }\n\n insert(rule: string): void {\n const reason = unusableReason(rule);\n\n if (reason !== null) {\n log.warn(`Dropped a malformed CSS rule that ${reason}: ${printableRule(rule)}`);\n\n return;\n }\n\n this.css += this.css.length > 0 ? `\\n${rule}` : rule;\n this.scheduleUpdate();\n }\n}\n\nexport { StyleSheet };\n"]}
1
+ {"version":3,"file":"stylesheet.js","sourceRoot":"","sources":["../src/stylesheet.ts"],"names":[],"mappings":"AACA,OAAO,EAAE,YAAY,EAAE,MAAM,aAAa,CAAC;AAC3C,OAAO,EAAE,wBAAwB,EAAE,iCAAiC,EAAE,MAAM,eAAe,CAAC;AAC5F,OAAO,EAAE,MAAM,EAAE,UAAU,EAAE,aAAa,EAAE,MAAM,gBAAgB,CAAC;AACnE,OAAO,EAAE,kBAAkB,EAAE,MAAM,qBAAqB,CAAC;AAEzD,MAAM,GAAG,GAAG,YAAY,CAAC,KAAK,CAAC,CAAC;AAEhC,MAAM,cAAc,GAAG,CAAC,IAAY,EAAiB,EAAE,CAAC,CAAC,MAAM,CAAC,IAAI,CAAC,CAAC,CAAC,CAAC,UAAU,CAAC,CAAC,CAAC,kBAAkB,CAAC,IAAI,CAAC,CAAC,CAAC;AAE/G,MAAM,UAAU;IACJ,GAAG,GAAG,EAAE,CAAC;IACT,QAAQ,GAA2B,IAAI,CAAC;IACxC,eAAe,GAAG,KAAK,CAAC;IAExB,cAAc;QAClB,IAAI,IAAI,CAAC,QAAQ,EAAE,CAAC;YAChB,OAAO,IAAI,CAAC,QAAQ,CAAC;QACzB,CAAC;QAED,MAAM,QAAQ,GAAG,iCAAiC,EAAE,CAAC;QACrD,IAAI,CAAC,QAAQ,GAAG,QAAQ,CAAC;QACzB,wBAAwB,CAAC,QAAQ,EAAE,GAAG,EAAE,KAAK,CAAC,CAAC;QAE/C,OAAO,QAAQ,CAAC;IACpB,CAAC;IAEO,cAAc;QAClB,IAAI,IAAI,CAAC,eAAe,EAAE,CAAC;YACvB,OAAO;QACX,CAAC;QAED,IAAI,CAAC,eAAe,GAAG,IAAI,CAAC;QAE5B,cAAc,CAAC,GAAG,EAAE;YAChB,IAAI,CAAC,eAAe,GAAG,KAAK,CAAC;YAC7B,IAAI,CAAC,cAAc,EAAE,CAAC,cAAc,CAAC,IAAI,CAAC,GAAG,CAAC,CAAC;QACnD,CAAC,CAAC,CAAC;IACP,CAAC;IAED,MAAM,CAAC,IAAY;QACf,MAAM,MAAM,GAAG,cAAc,CAAC,IAAI,CAAC,CAAC;QAEpC,IAAI,MAAM,KAAK,IAAI,EAAE,CAAC;YAClB,GAAG,CAAC,IAAI,CAAC,qCAAqC,MAAM,KAAK,aAAa,CAAC,IAAI,CAAC,EAAE,CAAC,CAAC;YAEhF,OAAO;QACX,CAAC;QAED,IAAI,CAAC,GAAG,IAAI,IAAI,CAAC,GAAG,CAAC,MAAM,GAAG,CAAC,CAAC,CAAC,CAAC,KAAK,IAAI,EAAE,CAAC,CAAC,CAAC,IAAI,CAAC;QACrD,IAAI,CAAC,cAAc,EAAE,CAAC;IAC1B,CAAC;CACJ;AAED,OAAO,EAAE,UAAU,EAAE,CAAC","sourcesContent":["import type * as Gtk from \"@gtkx/gi/gtk\";\nimport { createLogger } from \"@gtkx/utils\";\nimport { attachParsingErrorLogger, registerProviderForDefaultDisplay } from \"./provider.js\";\nimport { hasNul, NUL_REASON, printableRule } from \"./rule-text.js\";\nimport { containmentFailure } from \"./self-contained.js\";\n\nconst log = createLogger(\"css\");\n\nconst unusableReason = (rule: string): string | null => (hasNul(rule) ? NUL_REASON : containmentFailure(rule));\n\nclass StyleSheet {\n private css = \"\";\n private provider: Gtk.CssProvider | null = null;\n private updateScheduled = false;\n\n private ensureProvider(): Gtk.CssProvider {\n if (this.provider) {\n return this.provider;\n }\n\n const provider = registerProviderForDefaultDisplay();\n this.provider = provider;\n attachParsingErrorLogger(provider, log, \"CSS\");\n\n return provider;\n }\n\n private scheduleUpdate(): void {\n if (this.updateScheduled) {\n return;\n }\n\n this.updateScheduled = true;\n\n queueMicrotask(() => {\n this.updateScheduled = false;\n this.ensureProvider().loadFromString(this.css);\n });\n }\n\n insert(rule: string): void {\n const reason = unusableReason(rule);\n\n if (reason !== null) {\n log.warn(`Dropped a malformed CSS rule that ${reason}: ${printableRule(rule)}`);\n\n return;\n }\n\n this.css += this.css.length > 0 ? `\\n${rule}` : rule;\n this.scheduleUpdate();\n }\n}\n\nexport { StyleSheet };\n"]}
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@gtkx/css",
3
- "version": "1.2.2",
4
- "description": "Emotion-flavored CSS-in-JS that compiles to real GTK4 CSS and installs it on the default display",
3
+ "version": "1.4.0",
4
+ "description": "CSS-in-JS for GTK that compiles to real GTK4 CSS.",
5
5
  "keywords": [
6
6
  "gtkx",
7
7
  "gtk",
@@ -31,6 +31,10 @@
31
31
  ".": {
32
32
  "types": "./dist/index.d.ts",
33
33
  "default": "./dist/index.js"
34
+ },
35
+ "./internal": {
36
+ "types": "./dist/internal.d.ts",
37
+ "default": "./dist/internal.js"
34
38
  }
35
39
  },
36
40
  "sideEffects": false,
@@ -54,11 +58,11 @@
54
58
  "dependencies": {
55
59
  "@emotion/serialize": "^1.3.3",
56
60
  "stylis": "^4.4.0",
57
- "@gtkx/utils": "1.2.2"
61
+ "@gtkx/utils": "1.4.0"
58
62
  },
59
63
  "devDependencies": {
60
64
  "@types/stylis": "^4.2.7",
61
- "@gtkx/vitest": "1.2.2"
65
+ "@gtkx/vitest": "1.4.0"
62
66
  },
63
67
  "scripts": {
64
68
  "release": "tsx ../../scripts/release-package.ts"
package/src/create-css.ts CHANGED
@@ -1,8 +1,6 @@
1
1
  import type { CSSInterpolation, RegisteredCache, SerializedStyles } from "@emotion/serialize";
2
- import type { Element } from "stylis";
3
2
  import { serializeStyles } from "@emotion/serialize";
4
- import { compile, middleware, rulesheet, stringify, serialize as stylisSerialize } from "stylis";
5
- import { escapeNamedColors, restoreNamedColors } from "./named-colors.js";
3
+ import { eachRule, terminateDeclarations } from "./serialize-rule.js";
6
4
  import { StyleSheet } from "./stylesheet.js";
7
5
 
8
6
  /** A `cx` argument: a non-empty class name, or a boolean or nullish value that is dropped. */
@@ -28,32 +26,8 @@ type TokenPartition = { rawClasses: string[]; registeredStyles: string[] };
28
26
  type CssState = { sheet: StyleSheet; inserted: Set<string>; registered: RegisteredCache };
29
27
 
30
28
  const KEY = "gtkx";
31
- const LABEL_DECL_FIRST_CHAR = 108;
32
- const LABEL_DECL_THIRD_CHAR = 98;
33
-
34
- const removeLabel = (element: Element): void => {
35
- if (!(element.type === "decl" &&
36
- element.value.codePointAt(0) === LABEL_DECL_FIRST_CHAR &&
37
- element.value.codePointAt(2) === LABEL_DECL_THIRD_CHAR)) {
38
- return;
39
- }
40
-
41
- element.return = "";
42
- element.value = "";
43
- };
44
29
 
45
30
  const getClassName = (serialized: SerializedStyles): string => `${KEY}-${serialized.name}`;
46
-
47
- const terminateDeclarations = (styles: string): string => {
48
- const trimmed = styles.trimEnd();
49
-
50
- if (trimmed.length === 0 || trimmed.endsWith(";") || trimmed.endsWith("}")) {
51
- return trimmed;
52
- }
53
-
54
- return `${trimmed};`;
55
- };
56
-
57
31
  const isNonEmptyString = (token: CxToken): token is string => typeof token === "string" && token.length > 0;
58
32
 
59
33
  const partitionTokens = (tokens: string[], registered: RegisteredCache): TokenPartition => {
@@ -73,17 +47,10 @@ const partitionTokens = (tokens: string[], registered: RegisteredCache): TokenPa
73
47
  return { rawClasses, registeredStyles };
74
48
  };
75
49
 
76
- const runStylis = (sheet: StyleSheet, input: string): void => {
77
- stylisSerialize(
78
- compile(escapeNamedColors(input)),
79
- middleware([
80
- removeLabel,
81
- stringify,
82
- rulesheet((rule) => {
83
- sheet.insert(restoreNamedColors(rule));
84
- }),
85
- ]),
86
- );
50
+ const insertRules = (state: CssState, input: string): void => {
51
+ eachRule(input, (rule) => {
52
+ state.sheet.insert(rule);
53
+ });
87
54
  };
88
55
 
89
56
  const didMarkNewStyle = (state: CssState, serialized: SerializedStyles): boolean => {
@@ -103,7 +70,7 @@ const insertStyles = (state: CssState, serialized: SerializedStyles): void => {
103
70
 
104
71
  const className = getClassName(serialized);
105
72
  const styles = terminateDeclarations(serialized.styles);
106
- runStylis(state.sheet, `.${className}{${styles}}`);
73
+ insertRules(state, `.${className}{${styles}}`);
107
74
  state.registered[className] = styles;
108
75
  };
109
76
 
@@ -112,7 +79,7 @@ const insertWithoutScoping = (state: CssState, serialized: SerializedStyles): vo
112
79
  return;
113
80
  }
114
81
 
115
- runStylis(state.sheet, serialized.styles);
82
+ insertRules(state, serialized.styles);
116
83
  };
117
84
 
118
85
  const cssClassName = (state: CssState, args: CSSInterpolation[]): string => {
@@ -0,0 +1,6 @@
1
+ export {
2
+ attachParsingErrorLogger,
3
+ type ProviderOptions,
4
+ registerProviderForDefaultDisplay,
5
+ } from "./provider.js";
6
+ export { scopedRule } from "./scoped-rule.js";
package/src/provider.ts CHANGED
@@ -1,16 +1,61 @@
1
1
  import type { Logger } from "@gtkx/utils";
2
2
  import { type Display, DisplayManager } from "@gtkx/gi/gdk";
3
- import { CssProvider, STYLE_PROVIDER_PRIORITY_APPLICATION, StyleContext } from "@gtkx/gi/gtk";
3
+ import {
4
+ checkVersion,
5
+ CssProvider,
6
+ Settings,
7
+ STYLE_PROVIDER_PRIORITY_APPLICATION,
8
+ StyleContext,
9
+ } from "@gtkx/gi/gtk";
4
10
 
5
- const registerProviderForDefaultDisplay = (
6
- priority: number = STYLE_PROVIDER_PRIORITY_APPLICATION,
7
- ): CssProvider => {
11
+ type ProviderOptions = { priority: number; followsPreferences: boolean };
12
+
13
+ const REDUCED_MOTION_MINOR = 22;
14
+
15
+ const DEFAULT_OPTIONS: ProviderOptions = {
16
+ priority: STYLE_PROVIDER_PRIORITY_APPLICATION,
17
+ followsPreferences: true,
18
+ };
19
+
20
+ const hasReducedMotion = (): boolean => checkVersion(4, REDUCED_MOTION_MINOR, 0) === null;
21
+
22
+ const applyPreferences = (provider: CssProvider, settings: Settings): void => {
23
+ provider.prefersColorScheme = settings.gtkInterfaceColorScheme;
24
+ provider.prefersContrast = settings.gtkInterfaceContrast;
25
+
26
+ if (hasReducedMotion()) {
27
+ provider.prefersReducedMotion = settings.gtkInterfaceReducedMotion;
28
+ }
29
+ };
30
+
31
+ const followSystemPreferences = (provider: CssProvider, display: Display): void => {
32
+ const settings = Settings.getForDisplay(display);
33
+
34
+ const sync = (): void => {
35
+ applyPreferences(provider, settings);
36
+ };
37
+
38
+ sync();
39
+ settings.on("notify::gtk-interface-color-scheme", sync);
40
+ settings.on("notify::gtk-interface-contrast", sync);
41
+
42
+ if (hasReducedMotion()) {
43
+ settings.on("notify::gtk-interface-reduced-motion", sync);
44
+ }
45
+ };
46
+
47
+ const registerProviderForDefaultDisplay = (options?: Partial<ProviderOptions>): CssProvider => {
48
+ const { priority, followsPreferences } = { ...DEFAULT_OPTIONS, ...options };
8
49
  const provider = new CssProvider();
9
50
  const manager = DisplayManager.get();
10
51
 
11
52
  const attach = (display: Display): void => {
12
53
  // eslint-disable-next-line @typescript-eslint/no-deprecated
13
54
  StyleContext.addProviderForDisplay(display, provider, priority);
55
+
56
+ if (followsPreferences) {
57
+ followSystemPreferences(provider, display);
58
+ }
14
59
  };
15
60
 
16
61
  const initialDisplay = manager.getDefaultDisplay();
@@ -34,4 +79,4 @@ const attachParsingErrorLogger = (provider: CssProvider, log: Logger, subject: s
34
79
  }
35
80
  };
36
81
 
37
- export { registerProviderForDefaultDisplay, attachParsingErrorLogger };
82
+ export { type ProviderOptions, registerProviderForDefaultDisplay, attachParsingErrorLogger };
@@ -0,0 +1,7 @@
1
+ const NUL = "\u{0}";
2
+ const NUL_REASON = "carries a NUL byte, which GTK4 cannot load";
3
+
4
+ const hasNul = (rule: string): boolean => rule.includes(NUL);
5
+ const printableRule = (rule: string): string => rule.replaceAll(NUL, String.raw`\0`);
6
+
7
+ export { hasNul, NUL_REASON, printableRule };
@@ -0,0 +1,61 @@
1
+ import type { CSSObject } from "@emotion/serialize";
2
+ import { serializeStyles } from "@emotion/serialize";
3
+ import { createLogger } from "@gtkx/utils";
4
+ import { hasNul, NUL_REASON, printableRule } from "./rule-text.js";
5
+ import { eachRule, terminateDeclarations } from "./serialize-rule.js";
6
+
7
+ type Scope = { className: string; rules: string[] };
8
+
9
+ const log = createLogger("css");
10
+ const ESCAPE_REASON = "does not start from the widget it belongs to";
11
+ const NAME_CHAR = /[\w-]/;
12
+
13
+ const isScoped = (rule: string, className: string): boolean => {
14
+ const prefix = `.${className}`;
15
+
16
+ if (!rule.startsWith(prefix)) {
17
+ return false;
18
+ }
19
+
20
+ const next = rule.charAt(prefix.length);
21
+
22
+ return next !== "" && !NAME_CHAR.test(next);
23
+ };
24
+
25
+ const unusableReason = (rule: string, className: string): string | null => {
26
+ if (hasNul(rule)) {
27
+ return NUL_REASON;
28
+ }
29
+
30
+ return isScoped(rule, className) ? null : ESCAPE_REASON;
31
+ };
32
+
33
+ const collect = (scope: Scope, rule: string): void => {
34
+ const reason = unusableReason(rule, scope.className);
35
+
36
+ if (reason !== null) {
37
+ log.warn(`Dropped a style rule that ${reason}: ${printableRule(rule)}`);
38
+
39
+ return;
40
+ }
41
+
42
+ scope.rules.push(rule);
43
+ };
44
+
45
+ const scopedRule = (className: string, style: object): string => {
46
+ const declarations = terminateDeclarations(serializeStyles([style as CSSObject], {}).styles);
47
+
48
+ if (declarations.length === 0) {
49
+ return "";
50
+ }
51
+
52
+ const scope: Scope = { className, rules: [] };
53
+
54
+ eachRule(`.${className}{${declarations}}`, (rule) => {
55
+ collect(scope, rule);
56
+ });
57
+
58
+ return scope.rules.join("");
59
+ };
60
+
61
+ export { scopedRule };
@@ -0,0 +1,42 @@
1
+ import type { Element } from "stylis";
2
+ import { compile, middleware, rulesheet, stringify, serialize as stylisSerialize } from "stylis";
3
+ import { escapeNamedColors, restoreNamedColors } from "./named-colors.js";
4
+
5
+ const LABEL_DECL_FIRST_CHAR = 108;
6
+ const LABEL_DECL_THIRD_CHAR = 98;
7
+
8
+ const removeLabel = (element: Element): void => {
9
+ if (!(element.type === "decl" &&
10
+ element.value.codePointAt(0) === LABEL_DECL_FIRST_CHAR &&
11
+ element.value.codePointAt(2) === LABEL_DECL_THIRD_CHAR)) {
12
+ return;
13
+ }
14
+
15
+ element.return = "";
16
+ element.value = "";
17
+ };
18
+
19
+ const terminateDeclarations = (styles: string): string => {
20
+ const trimmed = styles.trimEnd();
21
+
22
+ if (trimmed.length === 0 || trimmed.endsWith(";") || trimmed.endsWith("}")) {
23
+ return trimmed;
24
+ }
25
+
26
+ return `${trimmed};`;
27
+ };
28
+
29
+ const eachRule = (input: string, visit: (rule: string) => void): void => {
30
+ stylisSerialize(
31
+ compile(escapeNamedColors(input)),
32
+ middleware([
33
+ removeLabel,
34
+ stringify,
35
+ rulesheet((rule) => {
36
+ visit(restoreNamedColors(rule));
37
+ }),
38
+ ]),
39
+ );
40
+ };
41
+
42
+ export { eachRule, terminateDeclarations };
package/src/stylesheet.ts CHANGED
@@ -1,16 +1,12 @@
1
1
  import type * as Gtk from "@gtkx/gi/gtk";
2
2
  import { createLogger } from "@gtkx/utils";
3
3
  import { attachParsingErrorLogger, registerProviderForDefaultDisplay } from "./provider.js";
4
+ import { hasNul, NUL_REASON, printableRule } from "./rule-text.js";
4
5
  import { containmentFailure } from "./self-contained.js";
5
6
 
6
7
  const log = createLogger("css");
7
- const NUL = "\u{0}";
8
- const NUL_REASON = "carries a NUL byte, which GTK4 cannot load";
9
8
 
10
- const unusableReason = (rule: string): string | null =>
11
- rule.includes(NUL) ? NUL_REASON : containmentFailure(rule);
12
-
13
- const printableRule = (rule: string): string => rule.replaceAll(NUL, String.raw`\0`);
9
+ const unusableReason = (rule: string): string | null => (hasNul(rule) ? NUL_REASON : containmentFailure(rule));
14
10
 
15
11
  class StyleSheet {
16
12
  private css = "";