@wix/origami-transforms 0.1.29 → 0.1.31
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/dist/cssPlugins/spx/calls.d.ts +3 -0
- package/dist/cssPlugins/spxThemeVariables/index.d.ts +6 -0
- package/dist/cssPlugins/spxThemeVariables/utils/breakpointUtils.d.ts +23 -0
- package/dist/cssPlugins/spxThemeVariables/utils/copyPlanUtils.d.ts +14 -0
- package/dist/cssPlugins/spxThemeVariables/utils/cssTreeUtils.d.ts +10 -0
- package/dist/cssPlugins/spxThemeVariables/utils/restateUtils.d.ts +9 -0
- package/dist/cssPlugins/spxThemeVariables/utils/sectionUtils.d.ts +6 -0
- package/dist/cssPlugins/spxThemeVariables/utils/stylesheetUtils.d.ts +10 -0
- package/dist/cssPlugins/spxThemeVariables/utils/themeUtils.d.ts +6 -0
- package/dist/cssPlugins/spxThemeVariables/utils/wrappingAtRuleUtils.d.ts +10 -0
- package/dist/cssPlugins/spxThemeVariables/utils/writeUtils.d.ts +17 -0
- package/package.json +3 -3
- package/src/cssPlugins/index.js +2 -0
- package/src/cssPlugins/spx/README.md +12 -3
- package/src/cssPlugins/spx/calls.js +26 -0
- package/src/cssPlugins/spx/cascade.js +23 -4
- package/src/cssPlugins/spxThemeVariables/README.md +14 -0
- package/src/cssPlugins/spxThemeVariables/index.js +36 -0
- package/src/cssPlugins/spxThemeVariables/utils/breakpointUtils.js +126 -0
- package/src/cssPlugins/spxThemeVariables/utils/copyPlanUtils.js +162 -0
- package/src/cssPlugins/spxThemeVariables/utils/cssTreeUtils.js +58 -0
- package/src/cssPlugins/spxThemeVariables/utils/restateUtils.js +134 -0
- package/src/cssPlugins/spxThemeVariables/utils/sectionUtils.js +32 -0
- package/src/cssPlugins/spxThemeVariables/utils/stylesheetUtils.js +98 -0
- package/src/cssPlugins/spxThemeVariables/utils/themeUtils.js +90 -0
- package/src/cssPlugins/spxThemeVariables/utils/wrappingAtRuleUtils.js +68 -0
- package/src/cssPlugins/spxThemeVariables/utils/writeUtils.js +98 -0
|
@@ -1 +1,4 @@
|
|
|
1
|
+
export const THEME_SPX: "--theme-spx(";
|
|
1
2
|
export function lowerCalls(declaration: import("postcss").Declaration, resolve: (value: number) => string, invalidCall: () => Error): void;
|
|
3
|
+
export function lowerThemeCalls(declaration: import("postcss").Declaration, resolve: (value: number, themeRefWidth: number) => string, invalidCall: () => Error): void;
|
|
4
|
+
export function toThemeCalls(value: string, themeRefWidth: number): string;
|
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
/** @typedef {typeof checks[number]} Check */
|
|
2
|
+
/** @type {import('../../transform/types.js').CssTransformPlugin<Check>} */
|
|
3
|
+
export const spxThemeVariablesPlugin: import("../../transform/types.js").CssTransformPlugin<Check>;
|
|
4
|
+
export type Check = (typeof checks)[number];
|
|
5
|
+
declare const checks: readonly [];
|
|
6
|
+
export {};
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
export function maxWidthOf(query: string): number | undefined;
|
|
2
|
+
export function breakpointOf(ancestors: readonly import("postcss").Node[]): string;
|
|
3
|
+
export function widestFirst(breakpoints: readonly string[]): string[];
|
|
4
|
+
export function themeBreakpointsOf({ breakpoints }: ThemeVariables): string[];
|
|
5
|
+
export function inheritedBreakpointsOf({ breakpoints }: ThemeVariables, breakpoint: string): string[];
|
|
6
|
+
export function cascadeAt({ rules }: ThemeVariables, cascadeBreakpoints: readonly string[]): Map<string, CascadedVariable>;
|
|
7
|
+
export function resolveRefWidths(breakpoints: readonly string[], ownRefWidths: Map<string, number>): Map<string, number>;
|
|
8
|
+
export function refWidthAt({ refWidthByBreakpoint }: ThemeVariables, breakpoint: string): number;
|
|
9
|
+
export type VariableMap = Map<string, import("postcss").Declaration>;
|
|
10
|
+
export type ThemeRule = {
|
|
11
|
+
scanned: import("./stylesheetUtils.js").ScannedRule;
|
|
12
|
+
variables: VariableMap;
|
|
13
|
+
};
|
|
14
|
+
export type ThemeVariables = {
|
|
15
|
+
rules: ThemeRule[];
|
|
16
|
+
breakpoints: string[];
|
|
17
|
+
refWidthByBreakpoint: Map<string, number>;
|
|
18
|
+
};
|
|
19
|
+
export type CascadedVariable = {
|
|
20
|
+
declaration: import("postcss").Declaration;
|
|
21
|
+
breakpoint: string;
|
|
22
|
+
index: number;
|
|
23
|
+
};
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
export function planBreakpoints(theme: ThemeVariables): BreakpointPlan;
|
|
2
|
+
export function planThemeCopies(theme: ThemeVariables, breakpointPlan: BreakpointPlan): ThemeCopies;
|
|
3
|
+
export type CascadedVariable = import("./breakpointUtils.js").CascadedVariable;
|
|
4
|
+
export type ThemeVariables = import("./breakpointUtils.js").ThemeVariables;
|
|
5
|
+
export type VariableMap = import("./breakpointUtils.js").VariableMap;
|
|
6
|
+
export type Declaration = import("postcss").Declaration;
|
|
7
|
+
export type ThemeCopies = Map<string, VariableMap>;
|
|
8
|
+
export type VariableReferences = Map<string, string[]>;
|
|
9
|
+
export type BreakpointPlan = Map<string, string[]>;
|
|
10
|
+
export type CopyCriteria = {
|
|
11
|
+
ownVariables: VariableMap;
|
|
12
|
+
scaleDependent: Set<string>;
|
|
13
|
+
widerCopied: Set<string>;
|
|
14
|
+
};
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
export const DESKTOP: "";
|
|
2
|
+
export function ancestorsOf(node: import("postcss").Node): import("postcss").Node[];
|
|
3
|
+
export function isAtRule(node: import("postcss").Node): node is import("postcss").AtRule;
|
|
4
|
+
export function collapseWhitespace(text: string): string;
|
|
5
|
+
export function isInContainerDefaults(ancestors: readonly import("postcss").Node[]): boolean;
|
|
6
|
+
export function isTopLevel(ancestors: readonly import("postcss").Node[]): ancestors is readonly import("postcss").AtRule[];
|
|
7
|
+
export function hasRootSelector(rule: import("postcss").Rule): boolean;
|
|
8
|
+
export function isCustomProperty(prop: string): boolean;
|
|
9
|
+
export function isMedia(node: import("postcss").Node): node is import("postcss").AtRule;
|
|
10
|
+
export function declarationsOf(rule: import("postcss").Rule): import("postcss").Declaration[];
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
export function restateThemeVariables(theme: ThemeVariables, breakpointPlan: BreakpointPlan): void;
|
|
2
|
+
export type CascadedVariable = import("./breakpointUtils.js").CascadedVariable;
|
|
3
|
+
export type ThemeVariables = import("./breakpointUtils.js").ThemeVariables;
|
|
4
|
+
export type BreakpointPlan = import("./copyPlanUtils.js").BreakpointPlan;
|
|
5
|
+
export type RestateTarget = {
|
|
6
|
+
breakpoint: string;
|
|
7
|
+
index: number;
|
|
8
|
+
refWidth: number;
|
|
9
|
+
};
|
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
/** @typedef {import('./stylesheetUtils.js').ScannedRule} ScannedRule */
|
|
2
|
+
/** @typedef {import('./wrappingAtRuleUtils.js').Section} Section */
|
|
3
|
+
export const CONTENT_MAX_WIDTH: "--content-max-width";
|
|
4
|
+
export function readSectionRules(layoutRules: readonly ScannedRule[]): Section[];
|
|
5
|
+
export type ScannedRule = import("./stylesheetUtils.js").ScannedRule;
|
|
6
|
+
export type Section = import("./wrappingAtRuleUtils.js").Section;
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
export function scanStylesheet(root: import("postcss").Root, siteFolder: string): StylesheetScan;
|
|
2
|
+
export type ScannedRule = {
|
|
3
|
+
rule: import("postcss").Rule;
|
|
4
|
+
breakpoint: string;
|
|
5
|
+
declarations: import("postcss").Declaration[];
|
|
6
|
+
};
|
|
7
|
+
export type StylesheetScan = {
|
|
8
|
+
themeRules: ScannedRule[];
|
|
9
|
+
layoutRules: ScannedRule[];
|
|
10
|
+
};
|
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
export function readThemeVariables(themeRules: readonly ScannedRule[]): ThemeVariables;
|
|
2
|
+
export function tagThemeVariables(themeRules: readonly ScannedRule[], theme: ThemeVariables): void;
|
|
3
|
+
export type ThemeRule = import("./breakpointUtils.js").ThemeRule;
|
|
4
|
+
export type ThemeVariables = import("./breakpointUtils.js").ThemeVariables;
|
|
5
|
+
export type VariableMap = import("./breakpointUtils.js").VariableMap;
|
|
6
|
+
export type ScannedRule = import("./stylesheetUtils.js").ScannedRule;
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
export function wrappingAtRulesOf(rule: import("postcss").Rule): import("postcss").AtRule[];
|
|
2
|
+
export function groupSectionsByWrappingAtRules(sections: readonly Section[]): SectionGroup[];
|
|
3
|
+
export type Section = {
|
|
4
|
+
selector: string;
|
|
5
|
+
wrappingAtRules: import("postcss").AtRule[];
|
|
6
|
+
};
|
|
7
|
+
export type SectionGroup = {
|
|
8
|
+
wrappingAtRules: import("postcss").AtRule[];
|
|
9
|
+
selectors: string[];
|
|
10
|
+
};
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
/** @typedef {import('./copyPlanUtils.js').ThemeCopies} ThemeCopies */
|
|
2
|
+
/** @typedef {import('./wrappingAtRuleUtils.js').Section} Section */
|
|
3
|
+
/** @typedef {import('./wrappingAtRuleUtils.js').SectionGroup} SectionGroup */
|
|
4
|
+
/**
|
|
5
|
+
* @typedef {object} BreakpointCopies
|
|
6
|
+
* @property {string} breakpoint
|
|
7
|
+
* @property {import('postcss').Declaration[]} declarations
|
|
8
|
+
*/
|
|
9
|
+
export const THEME_COPIES_LAYER: "origami-theme-copies";
|
|
10
|
+
export function writeCopies(root: import("postcss").Root, themeCopies: ThemeCopies, sections: readonly Section[]): void;
|
|
11
|
+
export type ThemeCopies = import("./copyPlanUtils.js").ThemeCopies;
|
|
12
|
+
export type Section = import("./wrappingAtRuleUtils.js").Section;
|
|
13
|
+
export type SectionGroup = import("./wrappingAtRuleUtils.js").SectionGroup;
|
|
14
|
+
export type BreakpointCopies = {
|
|
15
|
+
breakpoint: string;
|
|
16
|
+
declarations: import("postcss").Declaration[];
|
|
17
|
+
};
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@wix/origami-transforms",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.31",
|
|
4
4
|
"description": "Babel and PostCSS checks and transforms for Wix origami page files.",
|
|
5
5
|
"main": "./src/index.js",
|
|
6
6
|
"files": [
|
|
@@ -23,7 +23,7 @@
|
|
|
23
23
|
"@wix/origami-system-components": "1.10.6",
|
|
24
24
|
"postcss": "^8.5.3",
|
|
25
25
|
"postcss-custom-media": "^12.0.1",
|
|
26
|
-
"@wix/origami-bi": "0.
|
|
26
|
+
"@wix/origami-bi": "0.11.0"
|
|
27
27
|
},
|
|
28
28
|
"devDependencies": {
|
|
29
29
|
"@types/node": "^22.0.0",
|
|
@@ -71,5 +71,5 @@
|
|
|
71
71
|
},
|
|
72
72
|
"type": "module",
|
|
73
73
|
"types": "./dist/index.d.ts",
|
|
74
|
-
"falconPackageHash": "
|
|
74
|
+
"falconPackageHash": "eea52f454e6f8d604431d5d69e6ce0bae25496f2528518f4dbdb43dd"
|
|
75
75
|
}
|
package/src/cssPlugins/index.js
CHANGED
|
@@ -26,6 +26,7 @@ import { presetsPlugin } from './presets.js'
|
|
|
26
26
|
import { shapeDividersPlugin } from './shapeDividers.js'
|
|
27
27
|
import { layoutPlugin } from './layout/index.js'
|
|
28
28
|
import { spxPlugin } from './spx/index.js'
|
|
29
|
+
import { spxThemeVariablesPlugin } from './spxThemeVariables/index.js'
|
|
29
30
|
import { stackChildrenPlugin } from './stackChildren.js'
|
|
30
31
|
import { wixVectorUrlsPlugin } from './wixVectorUrls.js'
|
|
31
32
|
|
|
@@ -39,6 +40,7 @@ export const cssPlugins = Object.freeze([
|
|
|
39
40
|
presetsPlugin,
|
|
40
41
|
componentPositionPlugin,
|
|
41
42
|
layoutPlugin,
|
|
43
|
+
spxThemeVariablesPlugin,
|
|
42
44
|
spxPlugin,
|
|
43
45
|
shapeDividersPlugin,
|
|
44
46
|
wixVectorUrlsPlugin,
|
|
@@ -64,17 +64,26 @@ When settings are absent, expressions fall back to reference width 1280, scale m
|
|
|
64
64
|
viewport width 100vw, scrollbar width 0px, stoppers 0px / 99999px and no cap. Nested components inherit settings unless
|
|
65
65
|
they override them. As with any CSS custom property, an authored variable containing an
|
|
66
66
|
SPX expression resolves where the variable is declared; inheriting the variable does not
|
|
67
|
-
re-evaluate it using a child's settings.
|
|
67
|
+
re-evaluate it using a child's settings. So `../spxThemeVariables/` copies the SPX-dependent theme
|
|
68
|
+
variables onto capped containers.
|
|
69
|
+
|
|
70
|
+
## Theme calls
|
|
71
|
+
|
|
72
|
+
`../spxThemeVariables/` rewrites each theme `--spx(N)` as `--theme-spx(N, W)`, W being the theme's
|
|
73
|
+
reference width; authors never write it. It compiles like `--spx(N)` but divides by the literal W
|
|
74
|
+
instead of `var(--spx-ref-width, 1280)`, so a copied theme value keeps its theme size while still
|
|
75
|
+
following the section's cap and scaling factor. N must be a literal number and W a positive one.
|
|
68
76
|
|
|
69
77
|
## Files and boundaries
|
|
70
78
|
|
|
71
79
|
- `index.js` removes the SPX function definition and visits declarations.
|
|
72
80
|
- `cascade.js` translates resolver settings and generates browser-evaluated expressions.
|
|
73
|
-
- `calls.js` parses literal numeric
|
|
81
|
+
- `calls.js` parses literal numeric `--spx()` and `--theme-spx()` calls and rejects unsupported arguments.
|
|
74
82
|
|
|
75
83
|
Reference widths must be positive numbers and resolver modes must be `scale` or `fixed`.
|
|
76
84
|
Stoppers and `--content-max-width` are not validated.
|
|
77
85
|
|
|
78
86
|
`test/spx.browser.mjs` checks computed styles for the button example, nested overrides,
|
|
79
|
-
settings in separate stylesheets, cascade priority, fixed fractions, signs, zero, stoppers
|
|
87
|
+
settings in separate stylesheets, cascade priority, fixed fractions, signs, zero, stoppers, cap
|
|
88
|
+
and theme copies on capped sections.
|
|
80
89
|
Run it together with the container browser checks using the package's `test:browser` script.
|
|
@@ -1,5 +1,8 @@
|
|
|
1
1
|
import { fail } from './index.js'
|
|
2
2
|
const CALL = /--spx\(\s*(-?(?:\d+\.?\d*|\.\d+))\s*\)/g
|
|
3
|
+
const THEME_CALL = /--theme-spx\(\s*(-?(?:\d+\.?\d*|\.\d+))\s*,\s*(\d+\.?\d*|\.\d+)\s*\)/g
|
|
4
|
+
|
|
5
|
+
export const THEME_SPX = '--theme-spx('
|
|
3
6
|
|
|
4
7
|
/** Replace literal SPX calls, reporting unsupported arguments at their declaration.
|
|
5
8
|
* @param {import('postcss').Declaration} declaration
|
|
@@ -11,3 +14,26 @@ export const lowerCalls = (declaration, resolve, invalidCall) => {
|
|
|
11
14
|
declaration.value = declaration.value.replace(CALL, (_, literal) => resolve(Number(literal)))
|
|
12
15
|
if (declaration.value.includes('--spx(')) throw fail('non_literal_spx_argument', invalidCall())
|
|
13
16
|
}
|
|
17
|
+
|
|
18
|
+
/**
|
|
19
|
+
* @param {import('postcss').Declaration} declaration
|
|
20
|
+
* @param {(value: number, themeRefWidth: number) => string} resolve
|
|
21
|
+
* @param {() => Error} invalidCall
|
|
22
|
+
*/
|
|
23
|
+
export const lowerThemeCalls = (declaration, resolve, invalidCall) => {
|
|
24
|
+
if (!declaration.value.includes(THEME_SPX)) return
|
|
25
|
+
declaration.value = declaration.value.replace(THEME_CALL, (call, literal, themeRefWidthLiteral) => {
|
|
26
|
+
const themeRefWidth = Number(themeRefWidthLiteral)
|
|
27
|
+
if (themeRefWidth <= 0) return call
|
|
28
|
+
const number = Number(literal)
|
|
29
|
+
return resolve(number, themeRefWidth)
|
|
30
|
+
})
|
|
31
|
+
if (declaration.value.includes(THEME_SPX)) throw invalidCall()
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
/**
|
|
35
|
+
* @param {string} value
|
|
36
|
+
* @param {number} themeRefWidth
|
|
37
|
+
*/
|
|
38
|
+
export const toThemeCalls = (value, themeRefWidth) =>
|
|
39
|
+
value.replace(CALL, (_, literal) => `--theme-spx(${literal}, ${themeRefWidth})`)
|
|
@@ -1,16 +1,34 @@
|
|
|
1
1
|
import { fail } from './index.js'
|
|
2
|
-
import { lowerCalls } from './calls.js'
|
|
2
|
+
import { lowerCalls, lowerThemeCalls } from './calls.js'
|
|
3
3
|
|
|
4
4
|
// Keep arithmetic at the call site: an inherited derived custom property would already
|
|
5
5
|
// be computed against its parent's settings. Only the numeric resolver switch inherits.
|
|
6
6
|
const SCALE = 'var(--origami-spx-scale, 1)'
|
|
7
7
|
const FLOOR = `calc(0.5px * ${SCALE})`
|
|
8
|
-
/**
|
|
9
|
-
|
|
10
|
-
|
|
8
|
+
/**
|
|
9
|
+
* @param {string} cap
|
|
10
|
+
* @param {string} [divisor]
|
|
11
|
+
*/
|
|
12
|
+
const unitCappedAt = (cap, divisor = 'var(--spx-ref-width, 1280)') =>
|
|
13
|
+
`(${SCALE} * (clamp(var(--spx-stopper-min, 0px), var(--scaling-factor, 100vw), min(var(--spx-stopper-max, 99999px), ${cap})) - var(--scrollbar-width, 0px)) / ${divisor} + (1 - ${SCALE}) * 1px)`
|
|
11
14
|
const UNIT = unitCappedAt('var(--content-max-width, 99999px)')
|
|
12
15
|
const UNCAPPED_UNIT = unitCappedAt('99999px')
|
|
13
16
|
|
|
17
|
+
/** @param {import('postcss').Declaration} declaration */
|
|
18
|
+
const compileThemeCalls = declaration => {
|
|
19
|
+
const cap = declaration.prop === '--content-max-width' ? '99999px' : 'var(--content-max-width, 99999px)'
|
|
20
|
+
lowerThemeCalls(
|
|
21
|
+
declaration,
|
|
22
|
+
(number, themeRefWidth) => {
|
|
23
|
+
if (number === 0) return '0px'
|
|
24
|
+
const divisor = String(themeRefWidth)
|
|
25
|
+
const unit = unitCappedAt(cap, divisor)
|
|
26
|
+
return number < 0 ? `min(calc(-1 * ${FLOOR}), ${number} * ${unit})` : `max(${FLOOR}, ${number} * ${unit})`
|
|
27
|
+
},
|
|
28
|
+
() => declaration.error('SPX calls --theme-spx() without a literal number and a positive literal theme reference width')
|
|
29
|
+
)
|
|
30
|
+
}
|
|
31
|
+
|
|
14
32
|
/** Compile settings and calls in place, retaining their cascade and source locations.
|
|
15
33
|
* @param {import('postcss').Declaration} declaration
|
|
16
34
|
*/
|
|
@@ -35,6 +53,7 @@ export const compileCascadeDeclaration = declaration => {
|
|
|
35
53
|
) {
|
|
36
54
|
throw fail('invalid_spx_reference_width', declaration.error('SPX reference width must be positive'))
|
|
37
55
|
}
|
|
56
|
+
compileThemeCalls(declaration)
|
|
38
57
|
const unit = declaration.prop === '--content-max-width' ? UNCAPPED_UNIT : UNIT
|
|
39
58
|
lowerCalls(
|
|
40
59
|
declaration,
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
# Theme SPX variables follow each section's settings
|
|
2
|
+
|
|
3
|
+
A custom property resolves `--spx()` where it is declared, so theme variables on `:root` ignore a section's `--content-max-width`. Like Thunderbolt, this plugin re-declares them on each capped section.
|
|
4
|
+
|
|
5
|
+
- **Theme:** the `:root` rules from `src/global/themes/theme.css`, at the top level or under one `(max-width: Npx)` `@media`. Theme variables are their custom properties except the SPX settings. Without a site folder in the context, the plugin does nothing.
|
|
6
|
+
- **Cascade:** at a breakpoint, the theme value of a variable is the one CSS gives `:root` there: among desktop, that breakpoint and every wider one, the declaration that comes last in the stylesheet wins, whatever the widths. So a wider block written after a narrower one overrides it.
|
|
7
|
+
- **Restate:** a theme variable holding `--spx(` that a breakpoint inherits, through the cascade, from a breakpoint with another `--spx-ref-width` is re-declared on that breakpoint's last theme `:root`, widest breakpoint first, so it is tagged with the breakpoint's own reference width, like the `:root` value it had before. When a narrower breakpoint that sets the variable comes before that `:root`, the restatement goes in a new `:root` block of that breakpoint placed just before the narrower one, so the narrower value still wins. `var()` readers are not restated, and neither is a value written after that last `:root`, since the restatement would lose to it.
|
|
8
|
+
- **Tag:** each theme `--spx(N)` becomes `--theme-spx(N, W)`, W being that breakpoint's `--spx-ref-width`, so a section's own reference width never changes it.
|
|
9
|
+
- **Capped:** a section whose desktop rule sets `--content-max-width`.
|
|
10
|
+
- **Copied:** desktop copies every SPX-dependent variable (it holds `--theme-spx(` or reads one through `var()`, at any depth), except `--content-max-width`. Each breakpoint copies the variables whose cascade winner it sets that are SPX-dependent or override a wider copy, plus inherited ones that first become SPX-dependent there. Copies are written widest first, so each breakpoint copy holds the value `:root` has there.
|
|
11
|
+
- **Written:** one `@layer origami-theme-copies` at the top, after any `@charset` and `@import`, so it is the lowest layer: any section rule wins, and a copy still beats the value inherited from `:root`. Inside it, one `:where(#a, #b, …)` rule for desktop, then one `@media` per breakpoint, widest first. Sections inside `@scope`, `@supports` or `@container` get their own list, wrapped in a copy of those wrapping at-rules, so the copies keep the section's condition. Copies drop `!important`. Nothing is written without copies or capped sections.
|
|
12
|
+
- **Order:** 16, after layout (15) and before SPX (20), which compiles `--theme-spx()`.
|
|
13
|
+
|
|
14
|
+
Code: `index.js` runs `stylesheetUtils.js` (scan), `themeUtils.js` (read), `copyPlanUtils.js` (breakpoint order, shared by restate and plan), `restateUtils.js`, `themeUtils.js` (tag), `sectionUtils.js` (with `wrappingAtRuleUtils.js`), `copyPlanUtils.js`, then `writeUtils.js`. Tests: `test/spxThemeVariables.spec.js`.
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
import { planBreakpoints, planThemeCopies } from './utils/copyPlanUtils.js'
|
|
2
|
+
import { readSectionRules } from './utils/sectionUtils.js'
|
|
3
|
+
import { restateThemeVariables } from './utils/restateUtils.js'
|
|
4
|
+
import { scanStylesheet } from './utils/stylesheetUtils.js'
|
|
5
|
+
import { readThemeVariables, tagThemeVariables } from './utils/themeUtils.js'
|
|
6
|
+
import { writeCopies } from './utils/writeUtils.js'
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* @param {import('postcss').Root} root
|
|
10
|
+
* @param {string} siteFolder
|
|
11
|
+
*/
|
|
12
|
+
const copyThemeVariables = (root, siteFolder) => {
|
|
13
|
+
const { themeRules, layoutRules } = scanStylesheet(root, siteFolder)
|
|
14
|
+
const theme = readThemeVariables(themeRules)
|
|
15
|
+
const breakpointPlan = planBreakpoints(theme)
|
|
16
|
+
restateThemeVariables(theme, breakpointPlan)
|
|
17
|
+
tagThemeVariables(theme.rules.map(({ scanned }) => scanned), theme)
|
|
18
|
+
const sections = readSectionRules(layoutRules)
|
|
19
|
+
const themeCopies = planThemeCopies(theme, breakpointPlan)
|
|
20
|
+
writeCopies(root, themeCopies, sections)
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
const checks = /** @type {const} */ ([])
|
|
24
|
+
/** @typedef {typeof checks[number]} Check */
|
|
25
|
+
|
|
26
|
+
/** @type {import('../../transform/types.js').CssTransformPlugin<Check>} */
|
|
27
|
+
export const spxThemeVariablesPlugin = {
|
|
28
|
+
checks,
|
|
29
|
+
name: 'spx-theme-variables',
|
|
30
|
+
order: 16,
|
|
31
|
+
transform(root, context) {
|
|
32
|
+
const siteFolder = context?.layout?.root
|
|
33
|
+
if (siteFolder === undefined) return
|
|
34
|
+
copyThemeVariables(root, siteFolder)
|
|
35
|
+
},
|
|
36
|
+
}
|
|
@@ -0,0 +1,126 @@
|
|
|
1
|
+
import { DESKTOP, collapseWhitespace, isMedia } from './cssTreeUtils.js'
|
|
2
|
+
|
|
3
|
+
/** @typedef {Map<string, import('postcss').Declaration>} VariableMap */
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* @typedef {object} ThemeRule
|
|
7
|
+
* @property {import('./stylesheetUtils.js').ScannedRule} scanned
|
|
8
|
+
* @property {VariableMap} variables
|
|
9
|
+
*/
|
|
10
|
+
|
|
11
|
+
/**
|
|
12
|
+
* @typedef {object} ThemeVariables
|
|
13
|
+
* @property {ThemeRule[]} rules
|
|
14
|
+
* @property {string[]} breakpoints
|
|
15
|
+
* @property {Map<string, number>} refWidthByBreakpoint
|
|
16
|
+
*/
|
|
17
|
+
|
|
18
|
+
/**
|
|
19
|
+
* @typedef {object} CascadedVariable
|
|
20
|
+
* @property {import('postcss').Declaration} declaration
|
|
21
|
+
* @property {string} breakpoint
|
|
22
|
+
* @property {number} index
|
|
23
|
+
*/
|
|
24
|
+
|
|
25
|
+
const DEFAULT_REF_WIDTH = 1280
|
|
26
|
+
|
|
27
|
+
const MAX_WIDTH = /^\(\s*max-width\s*:\s*(\d+(?:\.\d+)?)px\s*\)$/i
|
|
28
|
+
|
|
29
|
+
/** @param {string} query */
|
|
30
|
+
export const maxWidthOf = query => {
|
|
31
|
+
const trimmed = query.trim()
|
|
32
|
+
const match = MAX_WIDTH.exec(trimmed)
|
|
33
|
+
return match === null ? undefined : Number(match[1])
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
/** @param {string} query */
|
|
37
|
+
const breakpointKeyOf = query => {
|
|
38
|
+
const maxWidth = maxWidthOf(query)
|
|
39
|
+
return maxWidth === undefined ? collapseWhitespace(query) : `(max-width: ${maxWidth}px)`
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
/** @param {readonly import('postcss').Node[]} ancestors */
|
|
43
|
+
export const breakpointOf = ancestors => {
|
|
44
|
+
const media = ancestors.find(isMedia)
|
|
45
|
+
return media === undefined ? DESKTOP : breakpointKeyOf(media.params)
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
/** @param {readonly string[]} breakpoints */
|
|
49
|
+
export const widestFirst = breakpoints =>
|
|
50
|
+
[...breakpoints].sort((left, right) => (maxWidthOf(right) ?? 0) - (maxWidthOf(left) ?? 0))
|
|
51
|
+
|
|
52
|
+
/** @param {ThemeVariables} theme */
|
|
53
|
+
export const themeBreakpointsOf = ({ breakpoints }) => breakpoints
|
|
54
|
+
|
|
55
|
+
/**
|
|
56
|
+
* @param {readonly string[]} breakpoints
|
|
57
|
+
* @param {string} breakpoint
|
|
58
|
+
*/
|
|
59
|
+
const enclosingBreakpointsOf = (breakpoints, breakpoint) => {
|
|
60
|
+
const maxWidth = maxWidthOf(breakpoint)
|
|
61
|
+
if (maxWidth === undefined) return [breakpoint]
|
|
62
|
+
return breakpoints.filter(candidate => (maxWidthOf(candidate) ?? -1) >= maxWidth)
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
/**
|
|
66
|
+
* @param {ThemeVariables} theme
|
|
67
|
+
* @param {string} breakpoint
|
|
68
|
+
*/
|
|
69
|
+
export const inheritedBreakpointsOf = ({ breakpoints }, breakpoint) => {
|
|
70
|
+
if (breakpoint === DESKTOP) return []
|
|
71
|
+
const enclosing = enclosingBreakpointsOf(breakpoints, breakpoint)
|
|
72
|
+
const outer = enclosing.filter(candidate => candidate !== breakpoint)
|
|
73
|
+
return [DESKTOP, ...outer]
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
/**
|
|
77
|
+
* @param {ThemeRule} rule
|
|
78
|
+
* @param {number} index
|
|
79
|
+
* @returns {[string, CascadedVariable][]}
|
|
80
|
+
*/
|
|
81
|
+
const cascadedVariablesOf = ({ scanned, variables }, index) => {
|
|
82
|
+
const { breakpoint } = scanned
|
|
83
|
+
return [...variables].map(([prop, declaration]) => [prop, { declaration, breakpoint, index }])
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
/**
|
|
87
|
+
* @param {ThemeVariables} theme
|
|
88
|
+
* @param {readonly string[]} cascadeBreakpoints
|
|
89
|
+
* @returns {Map<string, CascadedVariable>}
|
|
90
|
+
*/
|
|
91
|
+
export const cascadeAt = ({ rules }, cascadeBreakpoints) => {
|
|
92
|
+
const matching = new Set(cascadeBreakpoints)
|
|
93
|
+
const entries = rules.flatMap((rule, index) => (matching.has(rule.scanned.breakpoint) ? cascadedVariablesOf(rule, index) : []))
|
|
94
|
+
return new Map(entries)
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
/**
|
|
98
|
+
* @param {readonly string[]} breakpoints
|
|
99
|
+
* @param {Map<string, number>} ownRefWidths
|
|
100
|
+
* @param {string} breakpoint
|
|
101
|
+
*/
|
|
102
|
+
const resolveRefWidth = (breakpoints, ownRefWidths, breakpoint) => {
|
|
103
|
+
const own = ownRefWidths.get(breakpoint)
|
|
104
|
+
if (own !== undefined) return own
|
|
105
|
+
const enclosing = enclosingBreakpointsOf(breakpoints, breakpoint)
|
|
106
|
+
const defined = enclosing.flatMap(candidate => ownRefWidths.get(candidate) ?? [])
|
|
107
|
+
return defined.at(-1) ?? ownRefWidths.get(DESKTOP) ?? DEFAULT_REF_WIDTH
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
/**
|
|
111
|
+
* @param {readonly string[]} breakpoints
|
|
112
|
+
* @param {Map<string, number>} ownRefWidths
|
|
113
|
+
*/
|
|
114
|
+
export const resolveRefWidths = (breakpoints, ownRefWidths) => {
|
|
115
|
+
const entries = [DESKTOP, ...breakpoints].map(breakpoint => {
|
|
116
|
+
const refWidth = resolveRefWidth(breakpoints, ownRefWidths, breakpoint)
|
|
117
|
+
return /** @type {const} */ ([breakpoint, refWidth])
|
|
118
|
+
})
|
|
119
|
+
return new Map(entries)
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
/**
|
|
123
|
+
* @param {ThemeVariables} theme
|
|
124
|
+
* @param {string} breakpoint
|
|
125
|
+
*/
|
|
126
|
+
export const refWidthAt = ({ refWidthByBreakpoint }, breakpoint) => refWidthByBreakpoint.get(breakpoint) ?? DEFAULT_REF_WIDTH
|
|
@@ -0,0 +1,162 @@
|
|
|
1
|
+
import { THEME_SPX } from '../../spx/calls.js'
|
|
2
|
+
import { cascadeAt, inheritedBreakpointsOf, themeBreakpointsOf, widestFirst } from './breakpointUtils.js'
|
|
3
|
+
import { DESKTOP } from './cssTreeUtils.js'
|
|
4
|
+
import { CONTENT_MAX_WIDTH } from './sectionUtils.js'
|
|
5
|
+
|
|
6
|
+
/** @typedef {import('./breakpointUtils.js').CascadedVariable} CascadedVariable */
|
|
7
|
+
/** @typedef {import('./breakpointUtils.js').ThemeVariables} ThemeVariables */
|
|
8
|
+
/** @typedef {import('./breakpointUtils.js').VariableMap} VariableMap */
|
|
9
|
+
/** @typedef {import('postcss').Declaration} Declaration */
|
|
10
|
+
/** @typedef {Map<string, VariableMap>} ThemeCopies */
|
|
11
|
+
/** @typedef {Map<string, string[]>} VariableReferences */
|
|
12
|
+
/** @typedef {Map<string, string[]>} BreakpointPlan */
|
|
13
|
+
|
|
14
|
+
const VAR_REFERENCE = /var\(\s*(--[\w-]+)/g
|
|
15
|
+
|
|
16
|
+
/** @param {ThemeVariables} theme */
|
|
17
|
+
const getPlanOrder = theme => {
|
|
18
|
+
const breakpoints = themeBreakpointsOf(theme)
|
|
19
|
+
const widest = widestFirst(breakpoints)
|
|
20
|
+
return [DESKTOP, ...widest]
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
/**
|
|
24
|
+
* @param {ThemeVariables} theme
|
|
25
|
+
* @returns {BreakpointPlan}
|
|
26
|
+
*/
|
|
27
|
+
export const planBreakpoints = theme => {
|
|
28
|
+
const breakpoints = getPlanOrder(theme)
|
|
29
|
+
const entries = breakpoints.map(breakpoint => {
|
|
30
|
+
const inheritedBreakpoints = inheritedBreakpointsOf(theme, breakpoint)
|
|
31
|
+
return /** @type {const} */ ([breakpoint, inheritedBreakpoints])
|
|
32
|
+
})
|
|
33
|
+
return new Map(entries)
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
/** @param {Declaration} declaration */
|
|
37
|
+
const getReferencedNames = declaration => {
|
|
38
|
+
const matches = [...declaration.value.matchAll(VAR_REFERENCE)]
|
|
39
|
+
return matches.map(([, name]) => name)
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
/**
|
|
43
|
+
* @param {VariableMap} variables
|
|
44
|
+
* @returns {VariableReferences}
|
|
45
|
+
*/
|
|
46
|
+
const buildReferenceMap = variables => {
|
|
47
|
+
/** @type {VariableReferences} */
|
|
48
|
+
const references = new Map()
|
|
49
|
+
for (const [prop, declaration] of variables) {
|
|
50
|
+
const names = getReferencedNames(declaration)
|
|
51
|
+
references.set(prop, names)
|
|
52
|
+
}
|
|
53
|
+
return references
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
/**
|
|
57
|
+
* @param {Set<string>} targets
|
|
58
|
+
* @param {readonly string[]} names
|
|
59
|
+
*/
|
|
60
|
+
const readsAny = (targets, names) => names.some(name => targets.has(name))
|
|
61
|
+
|
|
62
|
+
/**
|
|
63
|
+
* @param {VariableReferences} references
|
|
64
|
+
* @param {Set<string>} seeds
|
|
65
|
+
*/
|
|
66
|
+
const collectReaders = (references, seeds) => {
|
|
67
|
+
const reached = new Set(seeds)
|
|
68
|
+
let previousSize = -1
|
|
69
|
+
while (reached.size !== previousSize) {
|
|
70
|
+
previousSize = reached.size
|
|
71
|
+
for (const [prop, names] of references) {
|
|
72
|
+
if (readsAny(reached, names)) reached.add(prop)
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
return reached
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
/** @param {VariableMap} variables */
|
|
79
|
+
const findScaleDependent = variables => {
|
|
80
|
+
const scaledVariables = [...variables].filter(([, declaration]) => declaration.value.includes(THEME_SPX))
|
|
81
|
+
const scaledProps = scaledVariables.map(([prop]) => prop)
|
|
82
|
+
const scaled = new Set(scaledProps)
|
|
83
|
+
const references = buildReferenceMap(variables)
|
|
84
|
+
return collectReaders(references, scaled)
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
/**
|
|
88
|
+
* @param {Map<string, CascadedVariable>} cascaded
|
|
89
|
+
* @returns {VariableMap}
|
|
90
|
+
*/
|
|
91
|
+
const mergeCascade = cascaded => {
|
|
92
|
+
const variables = [...cascaded].map(([prop, { declaration }]) => /** @type {const} */ ([prop, declaration]))
|
|
93
|
+
return new Map(variables)
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
/**
|
|
97
|
+
* @param {Map<string, CascadedVariable>} cascaded
|
|
98
|
+
* @param {string} breakpoint
|
|
99
|
+
* @returns {VariableMap}
|
|
100
|
+
*/
|
|
101
|
+
const ownVariablesOf = (cascaded, breakpoint) => {
|
|
102
|
+
const own = [...cascaded].filter(([, cascadedVariable]) => cascadedVariable.breakpoint === breakpoint)
|
|
103
|
+
const variables = own.map(([prop, { declaration }]) => /** @type {const} */ ([prop, declaration]))
|
|
104
|
+
return new Map(variables)
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
/**
|
|
108
|
+
* @param {ThemeCopies} themeCopies
|
|
109
|
+
* @param {readonly string[]} inheritedBreakpoints
|
|
110
|
+
*/
|
|
111
|
+
const getWiderCopiedNames = (themeCopies, inheritedBreakpoints) => {
|
|
112
|
+
const props = inheritedBreakpoints.flatMap(breakpoint => {
|
|
113
|
+
const copies = themeCopies.get(breakpoint) ?? new Map()
|
|
114
|
+
return [...copies.keys()]
|
|
115
|
+
})
|
|
116
|
+
return new Set(props)
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
/**
|
|
120
|
+
* @typedef {object} CopyCriteria
|
|
121
|
+
* @property {VariableMap} ownVariables
|
|
122
|
+
* @property {Set<string>} scaleDependent
|
|
123
|
+
* @property {Set<string>} widerCopied
|
|
124
|
+
*/
|
|
125
|
+
|
|
126
|
+
/**
|
|
127
|
+
* @param {string} prop
|
|
128
|
+
* @param {CopyCriteria} criteria
|
|
129
|
+
*/
|
|
130
|
+
const shouldCopy = (prop, { ownVariables, scaleDependent, widerCopied }) =>
|
|
131
|
+
ownVariables.has(prop)
|
|
132
|
+
? scaleDependent.has(prop) || widerCopied.has(prop)
|
|
133
|
+
: scaleDependent.has(prop) && !widerCopied.has(prop)
|
|
134
|
+
|
|
135
|
+
/**
|
|
136
|
+
* @param {string} prop
|
|
137
|
+
* @param {CopyCriteria} criteria
|
|
138
|
+
*/
|
|
139
|
+
const shouldCopyVariable = (prop, criteria) => prop !== CONTENT_MAX_WIDTH && shouldCopy(prop, criteria)
|
|
140
|
+
|
|
141
|
+
/**
|
|
142
|
+
* @param {ThemeVariables} theme
|
|
143
|
+
* @param {BreakpointPlan} breakpointPlan
|
|
144
|
+
* @returns {ThemeCopies}
|
|
145
|
+
*/
|
|
146
|
+
export const planThemeCopies = (theme, breakpointPlan) => {
|
|
147
|
+
/** @type {ThemeCopies} */
|
|
148
|
+
const themeCopies = new Map()
|
|
149
|
+
for (const [breakpoint, inheritedBreakpoints] of breakpointPlan) {
|
|
150
|
+
const cascadeBreakpoints = [...inheritedBreakpoints, breakpoint]
|
|
151
|
+
const cascaded = cascadeAt(theme, cascadeBreakpoints)
|
|
152
|
+
const ownVariables = ownVariablesOf(cascaded, breakpoint)
|
|
153
|
+
const effectiveVariables = mergeCascade(cascaded)
|
|
154
|
+
const scaleDependent = findScaleDependent(effectiveVariables)
|
|
155
|
+
const widerCopied = getWiderCopiedNames(themeCopies, inheritedBreakpoints)
|
|
156
|
+
const criteria = { ownVariables, scaleDependent, widerCopied }
|
|
157
|
+
const copiedEntries = [...effectiveVariables].filter(([prop]) => shouldCopyVariable(prop, criteria))
|
|
158
|
+
const copies = new Map(copiedEntries)
|
|
159
|
+
themeCopies.set(breakpoint, copies)
|
|
160
|
+
}
|
|
161
|
+
return themeCopies
|
|
162
|
+
}
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
import { DEFAULTS_LAYER } from '../../layout/contract.js'
|
|
2
|
+
|
|
3
|
+
export const DESKTOP = ''
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* @param {import('postcss').Node} node
|
|
7
|
+
* @returns {import('postcss').Node[]}
|
|
8
|
+
*/
|
|
9
|
+
export const ancestorsOf = node => {
|
|
10
|
+
/** @type {import('postcss').Node[]} */
|
|
11
|
+
const ancestors = []
|
|
12
|
+
let current = node.parent
|
|
13
|
+
while (current !== undefined && current.type !== 'root' && current.type !== 'document') {
|
|
14
|
+
ancestors.push(current)
|
|
15
|
+
current = current.parent
|
|
16
|
+
}
|
|
17
|
+
return ancestors
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
/**
|
|
21
|
+
* @param {import('postcss').Node} node
|
|
22
|
+
* @returns {node is import('postcss').AtRule}
|
|
23
|
+
*/
|
|
24
|
+
export const isAtRule = node => node.type === 'atrule'
|
|
25
|
+
|
|
26
|
+
/** @param {string} text */
|
|
27
|
+
export const collapseWhitespace = text => text.replace(/\s+/g, ' ').trim()
|
|
28
|
+
|
|
29
|
+
/**
|
|
30
|
+
* @param {import('postcss').Node} node
|
|
31
|
+
* @param {string} name
|
|
32
|
+
*/
|
|
33
|
+
const isLayerNamed = (node, name) => isAtRule(node) && node.name === 'layer' && node.params.trim() === name
|
|
34
|
+
|
|
35
|
+
/** @param {readonly import('postcss').Node[]} ancestors */
|
|
36
|
+
export const isInContainerDefaults = ancestors => ancestors.some(node => isLayerNamed(node, DEFAULTS_LAYER))
|
|
37
|
+
|
|
38
|
+
/** @param {readonly import('postcss').Node[]} ancestors */
|
|
39
|
+
export const isTopLevel = ancestors => ancestors.every(isAtRule)
|
|
40
|
+
|
|
41
|
+
/** @param {import('postcss').Rule} rule */
|
|
42
|
+
export const hasRootSelector = rule => rule.selector.trim() === ':root'
|
|
43
|
+
|
|
44
|
+
/** @param {string} prop */
|
|
45
|
+
export const isCustomProperty = prop => prop.startsWith('--')
|
|
46
|
+
|
|
47
|
+
/**
|
|
48
|
+
* @param {import('postcss').Node} node
|
|
49
|
+
* @returns {node is import('postcss').AtRule}
|
|
50
|
+
*/
|
|
51
|
+
export const isMedia = node => isAtRule(node) && node.name === 'media'
|
|
52
|
+
|
|
53
|
+
/**
|
|
54
|
+
* @param {import('postcss').Rule} rule
|
|
55
|
+
* @returns {import('postcss').Declaration[]}
|
|
56
|
+
*/
|
|
57
|
+
export const declarationsOf = rule =>
|
|
58
|
+
rule.nodes.filter(/** @returns {node is import('postcss').Declaration} */ node => node.type === 'decl')
|
|
@@ -0,0 +1,134 @@
|
|
|
1
|
+
import { cascadeAt, maxWidthOf, refWidthAt } from './breakpointUtils.js'
|
|
2
|
+
import { isAtRule } from './cssTreeUtils.js'
|
|
3
|
+
|
|
4
|
+
/** @typedef {import('./breakpointUtils.js').CascadedVariable} CascadedVariable */
|
|
5
|
+
/** @typedef {import('./breakpointUtils.js').ThemeVariables} ThemeVariables */
|
|
6
|
+
/** @typedef {import('./copyPlanUtils.js').BreakpointPlan} BreakpointPlan */
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* @typedef {object} RestateTarget
|
|
10
|
+
* @property {string} breakpoint
|
|
11
|
+
* @property {number} index
|
|
12
|
+
* @property {number} refWidth
|
|
13
|
+
*/
|
|
14
|
+
|
|
15
|
+
const SPX_CALL = '--spx('
|
|
16
|
+
|
|
17
|
+
/**
|
|
18
|
+
* @param {ThemeVariables} theme
|
|
19
|
+
* @param {string} breakpoint
|
|
20
|
+
*/
|
|
21
|
+
const lastIndexAt = ({ rules }, breakpoint) => {
|
|
22
|
+
const indexes = rules.flatMap(({ scanned }, index) => (scanned.breakpoint === breakpoint ? [index] : []))
|
|
23
|
+
return indexes.at(-1)
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
/**
|
|
27
|
+
* @param {ThemeVariables} theme
|
|
28
|
+
* @param {RestateTarget} target
|
|
29
|
+
* @param {[string, CascadedVariable]} entry
|
|
30
|
+
*/
|
|
31
|
+
const needsRestate = (theme, target, [, source]) => {
|
|
32
|
+
if (source.breakpoint === target.breakpoint) return false
|
|
33
|
+
if (source.index > target.index) return false
|
|
34
|
+
if (!source.declaration.value.includes(SPX_CALL)) return false
|
|
35
|
+
const sourceRefWidth = refWidthAt(theme, source.breakpoint)
|
|
36
|
+
return sourceRefWidth !== target.refWidth
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
/**
|
|
40
|
+
* @param {ThemeVariables} theme
|
|
41
|
+
* @param {RestateTarget} target
|
|
42
|
+
* @param {[string, CascadedVariable]} entry
|
|
43
|
+
*/
|
|
44
|
+
const narrowerRuleIndexOf = ({ rules }, target, [prop, source]) => {
|
|
45
|
+
const targetWidth = maxWidthOf(target.breakpoint)
|
|
46
|
+
if (targetWidth === undefined) return -1
|
|
47
|
+
const sourceIndex = rules.findIndex(({ scanned }) => scanned.rule === source.declaration.parent)
|
|
48
|
+
const declaresNarrower = ({ scanned, variables }, index) => {
|
|
49
|
+
const width = maxWidthOf(scanned.breakpoint)
|
|
50
|
+
return index > sourceIndex && width !== undefined && width < targetWidth && variables.has(prop)
|
|
51
|
+
}
|
|
52
|
+
return rules.findIndex(declaresNarrower)
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
/**
|
|
56
|
+
* @param {import('./breakpointUtils.js').ThemeRule} target
|
|
57
|
+
* @param {[string, CascadedVariable]} entry
|
|
58
|
+
*/
|
|
59
|
+
const restateInto = ({ scanned, variables }, [prop, { declaration }]) => {
|
|
60
|
+
const clone = declaration.clone({ raws: {} })
|
|
61
|
+
scanned.rule.append(clone)
|
|
62
|
+
scanned.declarations.push(clone)
|
|
63
|
+
variables.set(prop, clone)
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
/**
|
|
67
|
+
* @param {import('postcss').Declaration} clone
|
|
68
|
+
* @param {import('postcss').Rule} targetRule
|
|
69
|
+
* @param {import('postcss').AtRule} targetMedia
|
|
70
|
+
*/
|
|
71
|
+
const wrapperOf = (clone, targetRule, targetMedia) => {
|
|
72
|
+
const rule = targetRule.clone({ raws: {} }).removeAll()
|
|
73
|
+
rule.append(clone)
|
|
74
|
+
const wrapper = targetMedia.clone({ raws: {} }).removeAll()
|
|
75
|
+
wrapper.append(rule)
|
|
76
|
+
return { rule, wrapper }
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
/**
|
|
80
|
+
* @param {ThemeVariables} theme
|
|
81
|
+
* @param {import('./breakpointUtils.js').ThemeRule} targetRule
|
|
82
|
+
* @param {number} index
|
|
83
|
+
* @param {[string, CascadedVariable]} entry
|
|
84
|
+
*/
|
|
85
|
+
const restateBefore = (theme, targetRule, index, entry) => {
|
|
86
|
+
const [prop, { declaration }] = entry
|
|
87
|
+
const targetMedia = targetRule.scanned.rule.parent
|
|
88
|
+
const narrowerMedia = theme.rules[index].scanned.rule.parent
|
|
89
|
+
if (targetMedia === undefined || narrowerMedia === undefined) return restateInto(targetRule, entry)
|
|
90
|
+
if (!isAtRule(targetMedia) || !isAtRule(narrowerMedia)) return restateInto(targetRule, entry)
|
|
91
|
+
const clone = declaration.clone({ raws: {} })
|
|
92
|
+
const { rule, wrapper } = wrapperOf(clone, targetRule.scanned.rule, targetMedia)
|
|
93
|
+
narrowerMedia.before(wrapper)
|
|
94
|
+
const { breakpoint } = targetRule.scanned
|
|
95
|
+
const scanned = { rule, breakpoint, declarations: [clone] }
|
|
96
|
+
theme.rules.splice(index, 0, { scanned, variables: new Map([[prop, clone]]) })
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
/**
|
|
100
|
+
* @param {ThemeVariables} theme
|
|
101
|
+
* @param {import('./breakpointUtils.js').ThemeRule} targetRule
|
|
102
|
+
* @param {RestateTarget} target
|
|
103
|
+
* @param {[string, CascadedVariable]} entry
|
|
104
|
+
*/
|
|
105
|
+
const restate = (theme, targetRule, target, entry) => {
|
|
106
|
+
const index = narrowerRuleIndexOf(theme, target, entry)
|
|
107
|
+
const targetIndex = theme.rules.indexOf(targetRule)
|
|
108
|
+
if (index === -1 || index >= targetIndex) return restateInto(targetRule, entry)
|
|
109
|
+
restateBefore(theme, targetRule, index, entry)
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
/**
|
|
113
|
+
* @param {ThemeVariables} theme
|
|
114
|
+
* @param {string} breakpoint
|
|
115
|
+
* @param {readonly string[]} inheritedBreakpoints
|
|
116
|
+
*/
|
|
117
|
+
const restateAt = (theme, breakpoint, inheritedBreakpoints) => {
|
|
118
|
+
const index = lastIndexAt(theme, breakpoint)
|
|
119
|
+
if (index === undefined) return
|
|
120
|
+
const refWidth = refWidthAt(theme, breakpoint)
|
|
121
|
+
const target = { breakpoint, index, refWidth }
|
|
122
|
+
const cascaded = cascadeAt(theme, [...inheritedBreakpoints, breakpoint])
|
|
123
|
+
const stale = [...cascaded].filter(entry => needsRestate(theme, target, entry))
|
|
124
|
+
const targetRule = theme.rules[index]
|
|
125
|
+
for (const entry of stale) restate(theme, targetRule, target, entry)
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
/**
|
|
129
|
+
* @param {ThemeVariables} theme
|
|
130
|
+
* @param {BreakpointPlan} breakpointPlan
|
|
131
|
+
*/
|
|
132
|
+
export const restateThemeVariables = (theme, breakpointPlan) => {
|
|
133
|
+
for (const [breakpoint, inheritedBreakpoints] of breakpointPlan) restateAt(theme, breakpoint, inheritedBreakpoints)
|
|
134
|
+
}
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
import { DESKTOP } from './cssTreeUtils.js'
|
|
2
|
+
import { wrappingAtRulesOf } from './wrappingAtRuleUtils.js'
|
|
3
|
+
|
|
4
|
+
/** @typedef {import('./stylesheetUtils.js').ScannedRule} ScannedRule */
|
|
5
|
+
/** @typedef {import('./wrappingAtRuleUtils.js').Section} Section */
|
|
6
|
+
|
|
7
|
+
export const CONTENT_MAX_WIDTH = '--content-max-width'
|
|
8
|
+
|
|
9
|
+
/** @param {ScannedRule} layoutRule */
|
|
10
|
+
const capsAtDesktop = ({ breakpoint, declarations }) => {
|
|
11
|
+
if (breakpoint !== DESKTOP) return false
|
|
12
|
+
return declarations.some(({ prop }) => prop === CONTENT_MAX_WIDTH)
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
/**
|
|
16
|
+
* @param {ScannedRule} layoutRule
|
|
17
|
+
* @returns {Section}
|
|
18
|
+
*/
|
|
19
|
+
const sectionOf = ({ rule }) => {
|
|
20
|
+
const selector = rule.selector.trim()
|
|
21
|
+
const wrappingAtRules = wrappingAtRulesOf(rule)
|
|
22
|
+
return { selector, wrappingAtRules }
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
/**
|
|
26
|
+
* @param {readonly ScannedRule[]} layoutRules
|
|
27
|
+
* @returns {Section[]}
|
|
28
|
+
*/
|
|
29
|
+
export const readSectionRules = layoutRules => {
|
|
30
|
+
const sectionsWithMaxWidthRules = layoutRules.filter(capsAtDesktop)
|
|
31
|
+
return sectionsWithMaxWidthRules.map(sectionOf)
|
|
32
|
+
}
|
|
@@ -0,0 +1,98 @@
|
|
|
1
|
+
import { breakpointOf, maxWidthOf } from './breakpointUtils.js'
|
|
2
|
+
import {
|
|
3
|
+
ancestorsOf,
|
|
4
|
+
declarationsOf,
|
|
5
|
+
hasRootSelector,
|
|
6
|
+
isInContainerDefaults,
|
|
7
|
+
isMedia,
|
|
8
|
+
isTopLevel,
|
|
9
|
+
} from './cssTreeUtils.js'
|
|
10
|
+
import { relativeToSite } from '../../../transform/relativeToSite.js'
|
|
11
|
+
|
|
12
|
+
/**
|
|
13
|
+
* @typedef {object} ScannedRule
|
|
14
|
+
* @property {import('postcss').Rule} rule
|
|
15
|
+
* @property {string} breakpoint
|
|
16
|
+
* @property {import('postcss').Declaration[]} declarations
|
|
17
|
+
*/
|
|
18
|
+
|
|
19
|
+
/**
|
|
20
|
+
* @typedef {object} StylesheetScan
|
|
21
|
+
* @property {ScannedRule[]} themeRules
|
|
22
|
+
* @property {ScannedRule[]} layoutRules
|
|
23
|
+
*/
|
|
24
|
+
|
|
25
|
+
const THEME_FILE = 'src/global/themes/theme.css'
|
|
26
|
+
|
|
27
|
+
/** @param {readonly import('postcss').Node[]} ancestors */
|
|
28
|
+
const isThemeContext = ancestors => {
|
|
29
|
+
const [parent] = ancestors
|
|
30
|
+
if (parent === undefined) return true
|
|
31
|
+
return ancestors.length === 1 && isMedia(parent) && maxWidthOf(parent.params) !== undefined
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
/**
|
|
35
|
+
* @param {import('postcss').Rule} rule
|
|
36
|
+
* @param {string} siteFolder
|
|
37
|
+
*/
|
|
38
|
+
const isFromThemeFile = (rule, siteFolder) => {
|
|
39
|
+
const file = rule.source?.input.file
|
|
40
|
+
if (file === undefined) return false
|
|
41
|
+
const relative = relativeToSite(siteFolder, file)
|
|
42
|
+
return relative === THEME_FILE
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
/** @param {readonly import('postcss').Node[]} ancestors */
|
|
46
|
+
const isLayoutRule = ancestors => isTopLevel(ancestors) && !isInContainerDefaults(ancestors)
|
|
47
|
+
|
|
48
|
+
/**
|
|
49
|
+
* @param {import('postcss').Rule} rule
|
|
50
|
+
* @param {import('postcss').Node[]} ancestors
|
|
51
|
+
* @returns {ScannedRule}
|
|
52
|
+
*/
|
|
53
|
+
const scannedRuleOf = (rule, ancestors) => {
|
|
54
|
+
const breakpoint = breakpointOf(ancestors)
|
|
55
|
+
const declarations = declarationsOf(rule)
|
|
56
|
+
return { rule, breakpoint, declarations }
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
/**
|
|
60
|
+
* @param {import('postcss').Rule} rule
|
|
61
|
+
* @param {string} siteFolder
|
|
62
|
+
* @returns {ScannedRule[]}
|
|
63
|
+
*/
|
|
64
|
+
const themeRuleOf = (rule, siteFolder) => {
|
|
65
|
+
if (!isFromThemeFile(rule, siteFolder)) return []
|
|
66
|
+
const ancestors = ancestorsOf(rule)
|
|
67
|
+
if (!isThemeContext(ancestors)) return []
|
|
68
|
+
return [scannedRuleOf(rule, ancestors)]
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
/**
|
|
72
|
+
* @param {import('postcss').Rule} rule
|
|
73
|
+
* @returns {ScannedRule[]}
|
|
74
|
+
*/
|
|
75
|
+
const layoutRuleOf = rule => {
|
|
76
|
+
const ancestors = ancestorsOf(rule)
|
|
77
|
+
return isLayoutRule(ancestors) ? [scannedRuleOf(rule, ancestors)] : []
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
/**
|
|
81
|
+
* @param {import('postcss').Root} root
|
|
82
|
+
* @param {string} siteFolder
|
|
83
|
+
* @returns {StylesheetScan}
|
|
84
|
+
*/
|
|
85
|
+
export const scanStylesheet = (root, siteFolder) => {
|
|
86
|
+
/** @type {StylesheetScan} */
|
|
87
|
+
const scan = { themeRules: [], layoutRules: [] }
|
|
88
|
+
root.walkRules(rule => {
|
|
89
|
+
if (hasRootSelector(rule)) {
|
|
90
|
+
const themeRules = themeRuleOf(rule, siteFolder)
|
|
91
|
+
scan.themeRules.push(...themeRules)
|
|
92
|
+
return
|
|
93
|
+
}
|
|
94
|
+
const layoutRules = layoutRuleOf(rule)
|
|
95
|
+
scan.layoutRules.push(...layoutRules)
|
|
96
|
+
})
|
|
97
|
+
return scan
|
|
98
|
+
}
|
|
@@ -0,0 +1,90 @@
|
|
|
1
|
+
import { toThemeCalls } from '../../spx/calls.js'
|
|
2
|
+
import { refWidthAt, resolveRefWidths } from './breakpointUtils.js'
|
|
3
|
+
import { DESKTOP, isCustomProperty } from './cssTreeUtils.js'
|
|
4
|
+
|
|
5
|
+
/** @typedef {import('./breakpointUtils.js').ThemeRule} ThemeRule */
|
|
6
|
+
/** @typedef {import('./breakpointUtils.js').ThemeVariables} ThemeVariables */
|
|
7
|
+
/** @typedef {import('./breakpointUtils.js').VariableMap} VariableMap */
|
|
8
|
+
/** @typedef {import('./stylesheetUtils.js').ScannedRule} ScannedRule */
|
|
9
|
+
|
|
10
|
+
const REF_WIDTH = '--spx-ref-width'
|
|
11
|
+
|
|
12
|
+
const SPX_SETTINGS = new Set([
|
|
13
|
+
REF_WIDTH,
|
|
14
|
+
'--spx-resolver-type',
|
|
15
|
+
'--spx-stopper-min',
|
|
16
|
+
'--spx-stopper-max',
|
|
17
|
+
'--scaling-factor',
|
|
18
|
+
'--scrollbar-width',
|
|
19
|
+
])
|
|
20
|
+
|
|
21
|
+
/** @param {string} prop */
|
|
22
|
+
const isThemeVariable = prop => isCustomProperty(prop) && !SPX_SETTINGS.has(prop)
|
|
23
|
+
|
|
24
|
+
/** @param {number} refWidth */
|
|
25
|
+
const isUsableRefWidth = refWidth => Number.isFinite(refWidth) && refWidth > 0
|
|
26
|
+
|
|
27
|
+
/** @param {readonly ScannedRule[]} themeRules */
|
|
28
|
+
const rootDeclarationsByBreakpointOf = themeRules => {
|
|
29
|
+
/** @type {Map<string, VariableMap>} */
|
|
30
|
+
const declarationsByBreakpoint = new Map()
|
|
31
|
+
for (const { breakpoint, declarations } of themeRules) {
|
|
32
|
+
const declarationMap = declarationsByBreakpoint.get(breakpoint) ?? new Map()
|
|
33
|
+
declarationsByBreakpoint.set(breakpoint, declarationMap)
|
|
34
|
+
for (const declaration of declarations) {
|
|
35
|
+
declarationMap.set(declaration.prop, declaration)
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
return declarationsByBreakpoint
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
/**
|
|
42
|
+
* @param {ScannedRule} scanned
|
|
43
|
+
* @returns {ThemeRule}
|
|
44
|
+
*/
|
|
45
|
+
const themeRuleOf = scanned => {
|
|
46
|
+
const themeVariables = scanned.declarations.filter(({ prop }) => isThemeVariable(prop))
|
|
47
|
+
const entries = themeVariables.map(declaration => /** @type {const} */ ([declaration.prop, declaration]))
|
|
48
|
+
const variables = new Map(entries)
|
|
49
|
+
return { scanned, variables }
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
/** @param {Map<string, VariableMap>} declarationsByBreakpoint */
|
|
53
|
+
const breakpointsOf = declarationsByBreakpoint => {
|
|
54
|
+
const breakpoints = [...declarationsByBreakpoint.keys()]
|
|
55
|
+
return breakpoints.filter(breakpoint => breakpoint !== DESKTOP)
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
/** @param {Map<string, VariableMap>} declarationsByBreakpoint */
|
|
59
|
+
const refWidthByBreakpointOf = declarationsByBreakpoint => {
|
|
60
|
+
const refWidths = [...declarationsByBreakpoint].flatMap(([breakpoint, declarationMap]) => {
|
|
61
|
+
const refWidth = Number(declarationMap.get(REF_WIDTH)?.value)
|
|
62
|
+
return isUsableRefWidth(refWidth) ? [/** @type {const} */ ([breakpoint, refWidth])] : []
|
|
63
|
+
})
|
|
64
|
+
return new Map(refWidths)
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
/**
|
|
68
|
+
* @param {readonly ScannedRule[]} themeRules
|
|
69
|
+
* @returns {ThemeVariables}
|
|
70
|
+
*/
|
|
71
|
+
export const readThemeVariables = themeRules => {
|
|
72
|
+
const declarationsByBreakpoint = rootDeclarationsByBreakpointOf(themeRules)
|
|
73
|
+
const rules = themeRules.map(themeRuleOf)
|
|
74
|
+
const breakpoints = breakpointsOf(declarationsByBreakpoint)
|
|
75
|
+
const ownRefWidths = refWidthByBreakpointOf(declarationsByBreakpoint)
|
|
76
|
+
const refWidthByBreakpoint = resolveRefWidths(breakpoints, ownRefWidths)
|
|
77
|
+
return { rules, breakpoints, refWidthByBreakpoint }
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
/**
|
|
81
|
+
* @param {readonly ScannedRule[]} themeRules
|
|
82
|
+
* @param {ThemeVariables} theme
|
|
83
|
+
*/
|
|
84
|
+
export const tagThemeVariables = (themeRules, theme) => {
|
|
85
|
+
for (const { breakpoint, declarations } of themeRules) {
|
|
86
|
+
const refWidth = refWidthAt(theme, breakpoint)
|
|
87
|
+
const variables = declarations.filter(({ prop }) => isThemeVariable(prop))
|
|
88
|
+
for (const declaration of variables) declaration.value = toThemeCalls(declaration.value, refWidth)
|
|
89
|
+
}
|
|
90
|
+
}
|
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
import { ancestorsOf, collapseWhitespace, isAtRule } from './cssTreeUtils.js'
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* @typedef {object} Section
|
|
5
|
+
* @property {string} selector
|
|
6
|
+
* @property {import('postcss').AtRule[]} wrappingAtRules
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
/**
|
|
10
|
+
* @typedef {object} SectionGroup
|
|
11
|
+
* @property {import('postcss').AtRule[]} wrappingAtRules
|
|
12
|
+
* @property {string[]} selectors
|
|
13
|
+
*/
|
|
14
|
+
|
|
15
|
+
const UNWRAPPED_KEY = ''
|
|
16
|
+
|
|
17
|
+
/**
|
|
18
|
+
* @param {import('postcss').Node} node
|
|
19
|
+
* @returns {node is import('postcss').AtRule}
|
|
20
|
+
*/
|
|
21
|
+
const isWrappingAtRule = node => isAtRule(node) && node.name !== 'layer'
|
|
22
|
+
|
|
23
|
+
/**
|
|
24
|
+
* @param {import('postcss').Rule} rule
|
|
25
|
+
* @returns {import('postcss').AtRule[]}
|
|
26
|
+
*/
|
|
27
|
+
export const wrappingAtRulesOf = rule => {
|
|
28
|
+
const ancestors = ancestorsOf(rule)
|
|
29
|
+
const wrappingAtRules = ancestors.filter(isWrappingAtRule)
|
|
30
|
+
return wrappingAtRules.reverse()
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
/** @param {import('postcss').AtRule} atRule */
|
|
34
|
+
const atRuleLabelOf = ({ name, params }) => {
|
|
35
|
+
const collapsed = collapseWhitespace(params)
|
|
36
|
+
return `@${name} ${collapsed}`
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
/** @param {readonly import('postcss').AtRule[]} wrappingAtRules */
|
|
40
|
+
const wrappingKeyOf = wrappingAtRules => {
|
|
41
|
+
const labels = wrappingAtRules.map(atRuleLabelOf)
|
|
42
|
+
return labels.join(' ')
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
/**
|
|
46
|
+
* @param {Map<string, SectionGroup>} groups
|
|
47
|
+
* @param {Section} section
|
|
48
|
+
*/
|
|
49
|
+
const addSectionToGroup = (groups, { selector, wrappingAtRules }) => {
|
|
50
|
+
const key = wrappingKeyOf(wrappingAtRules)
|
|
51
|
+
const group = groups.get(key) ?? { wrappingAtRules, selectors: [] }
|
|
52
|
+
if (!group.selectors.includes(selector)) group.selectors.push(selector)
|
|
53
|
+
groups.set(key, group)
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
/**
|
|
57
|
+
* @param {readonly Section[]} sections
|
|
58
|
+
* @returns {SectionGroup[]}
|
|
59
|
+
*/
|
|
60
|
+
export const groupSectionsByWrappingAtRules = sections => {
|
|
61
|
+
/** @type {Map<string, SectionGroup>} */
|
|
62
|
+
const groups = new Map()
|
|
63
|
+
for (const section of sections) addSectionToGroup(groups, section)
|
|
64
|
+
const unwrapped = groups.get(UNWRAPPED_KEY)
|
|
65
|
+
groups.delete(UNWRAPPED_KEY)
|
|
66
|
+
const wrapped = [...groups.values()]
|
|
67
|
+
return unwrapped === undefined ? wrapped : [unwrapped, ...wrapped]
|
|
68
|
+
}
|
|
@@ -0,0 +1,98 @@
|
|
|
1
|
+
import postcss from 'postcss'
|
|
2
|
+
import { DESKTOP } from './cssTreeUtils.js'
|
|
3
|
+
import { groupSectionsByWrappingAtRules } from './wrappingAtRuleUtils.js'
|
|
4
|
+
|
|
5
|
+
/** @typedef {import('./copyPlanUtils.js').ThemeCopies} ThemeCopies */
|
|
6
|
+
/** @typedef {import('./wrappingAtRuleUtils.js').Section} Section */
|
|
7
|
+
/** @typedef {import('./wrappingAtRuleUtils.js').SectionGroup} SectionGroup */
|
|
8
|
+
|
|
9
|
+
/**
|
|
10
|
+
* @typedef {object} BreakpointCopies
|
|
11
|
+
* @property {string} breakpoint
|
|
12
|
+
* @property {import('postcss').Declaration[]} declarations
|
|
13
|
+
*/
|
|
14
|
+
|
|
15
|
+
export const THEME_COPIES_LAYER = 'origami-theme-copies'
|
|
16
|
+
|
|
17
|
+
const LEADING_AT_RULES = new Set(['charset', 'import'])
|
|
18
|
+
|
|
19
|
+
/**
|
|
20
|
+
* @param {ThemeCopies} themeCopies
|
|
21
|
+
* @returns {BreakpointCopies[]}
|
|
22
|
+
*/
|
|
23
|
+
const breakpointCopiesOf = themeCopies => {
|
|
24
|
+
const planned = [...themeCopies].map(([breakpoint, copies]) => ({ breakpoint, declarations: [...copies.values()] }))
|
|
25
|
+
return planned.filter(({ declarations }) => declarations.length > 0)
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
/**
|
|
29
|
+
* @param {string} selector
|
|
30
|
+
* @param {BreakpointCopies} breakpointCopies
|
|
31
|
+
* @returns {import('postcss').ChildNode}
|
|
32
|
+
*/
|
|
33
|
+
const breakpointBlockOf = (selector, { breakpoint, declarations }) => {
|
|
34
|
+
const nodes = declarations.map(declaration => declaration.clone({ important: false, raws: {} }))
|
|
35
|
+
const rule = postcss.rule({ selector, nodes })
|
|
36
|
+
if (breakpoint === DESKTOP) return rule
|
|
37
|
+
return postcss.atRule({ name: 'media', params: breakpoint, nodes: [rule] })
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
/**
|
|
41
|
+
* @param {import('postcss').ChildNode[]} nodes
|
|
42
|
+
* @param {import('postcss').AtRule} wrappingAtRule
|
|
43
|
+
* @returns {import('postcss').ChildNode[]}
|
|
44
|
+
*/
|
|
45
|
+
const wrapInAtRuleClone = (nodes, wrappingAtRule) => {
|
|
46
|
+
const clone = wrappingAtRule.clone({ nodes: [] })
|
|
47
|
+
clone.append(nodes)
|
|
48
|
+
return [clone]
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
/**
|
|
52
|
+
* @param {readonly BreakpointCopies[]} allBreakpointCopies
|
|
53
|
+
* @param {SectionGroup} group
|
|
54
|
+
* @returns {import('postcss').ChildNode[]}
|
|
55
|
+
*/
|
|
56
|
+
const groupBlocksOf = (allBreakpointCopies, { wrappingAtRules, selectors }) => {
|
|
57
|
+
const selector = `:where(${selectors.join(', ')})`
|
|
58
|
+
const blocks = allBreakpointCopies.map(breakpointCopies => breakpointBlockOf(selector, breakpointCopies))
|
|
59
|
+
return wrappingAtRules.reduceRight(wrapInAtRuleClone, blocks)
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
/**
|
|
63
|
+
* @param {readonly BreakpointCopies[]} allBreakpointCopies
|
|
64
|
+
* @param {readonly SectionGroup[]} groups
|
|
65
|
+
*/
|
|
66
|
+
const copyLayerOf = (allBreakpointCopies, groups) => {
|
|
67
|
+
const nodes = groups.flatMap(group => groupBlocksOf(allBreakpointCopies, group))
|
|
68
|
+
return postcss.atRule({ name: 'layer', params: THEME_COPIES_LAYER, nodes })
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
/** @param {import('postcss').ChildNode} node */
|
|
72
|
+
const isLeadingAtRule = node => node.type === 'atrule' && LEADING_AT_RULES.has(node.name)
|
|
73
|
+
|
|
74
|
+
/**
|
|
75
|
+
* @param {import('postcss').Root} root
|
|
76
|
+
* @param {import('postcss').AtRule} layer
|
|
77
|
+
*/
|
|
78
|
+
const prependAfterLeadingAtRules = (root, layer) => {
|
|
79
|
+
const firstOther = root.nodes.find(node => node.type !== 'comment' && !isLeadingAtRule(node))
|
|
80
|
+
if (firstOther === undefined) {
|
|
81
|
+
root.append(layer)
|
|
82
|
+
return
|
|
83
|
+
}
|
|
84
|
+
firstOther.before(layer)
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
/**
|
|
88
|
+
* @param {import('postcss').Root} root
|
|
89
|
+
* @param {ThemeCopies} themeCopies
|
|
90
|
+
* @param {readonly Section[]} sections
|
|
91
|
+
*/
|
|
92
|
+
export const writeCopies = (root, themeCopies, sections) => {
|
|
93
|
+
const allBreakpointCopies = breakpointCopiesOf(themeCopies)
|
|
94
|
+
if (allBreakpointCopies.length === 0 || sections.length === 0) return
|
|
95
|
+
const groups = groupSectionsByWrappingAtRules(sections)
|
|
96
|
+
const layer = copyLayerOf(allBreakpointCopies, groups)
|
|
97
|
+
prependAfterLeadingAtRules(root, layer)
|
|
98
|
+
}
|