@wix/origami-transforms 0.1.28 → 0.1.30

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (75) hide show
  1. package/dist/cssPlugins/componentPosition.d.ts +6 -2
  2. package/dist/cssPlugins/customEnums.d.ts +5 -2
  3. package/dist/cssPlugins/customMedia.d.ts +7 -2
  4. package/dist/cssPlugins/fonts.d.ts +6 -2
  5. package/dist/cssPlugins/index.d.ts +2 -2
  6. package/dist/cssPlugins/layout/index.d.ts +7 -2
  7. package/dist/cssPlugins/mixins.d.ts +7 -2
  8. package/dist/cssPlugins/presets.d.ts +6 -2
  9. package/dist/cssPlugins/shapeDividers.d.ts +6 -2
  10. package/dist/cssPlugins/spx/calls.d.ts +3 -0
  11. package/dist/cssPlugins/spx/index.d.ts +7 -2
  12. package/dist/cssPlugins/spxThemeVariables/index.d.ts +6 -0
  13. package/dist/cssPlugins/spxThemeVariables/utils/breakpointUtils.d.ts +23 -0
  14. package/dist/cssPlugins/spxThemeVariables/utils/copyPlanUtils.d.ts +14 -0
  15. package/dist/cssPlugins/spxThemeVariables/utils/cssTreeUtils.d.ts +10 -0
  16. package/dist/cssPlugins/spxThemeVariables/utils/restateUtils.d.ts +9 -0
  17. package/dist/cssPlugins/spxThemeVariables/utils/sectionUtils.d.ts +6 -0
  18. package/dist/cssPlugins/spxThemeVariables/utils/stylesheetUtils.d.ts +10 -0
  19. package/dist/cssPlugins/spxThemeVariables/utils/themeUtils.d.ts +6 -0
  20. package/dist/cssPlugins/spxThemeVariables/utils/wrappingAtRuleUtils.d.ts +10 -0
  21. package/dist/cssPlugins/spxThemeVariables/utils/writeUtils.d.ts +17 -0
  22. package/dist/cssPlugins/stackChildren.d.ts +5 -2
  23. package/dist/cssPlugins/wixVectorUrls.d.ts +6 -2
  24. package/dist/jsxPlugins/index.d.ts +2 -2
  25. package/dist/jsxPlugins/mediaValidation/index.d.ts +5 -2
  26. package/dist/jsxPlugins/origamiCompatibility/index.d.ts +6 -2
  27. package/dist/jsxPlugins/pageSlots.d.ts +7 -6
  28. package/dist/jsxPlugins/validation/checks.d.ts +7 -7
  29. package/dist/jsxPlugins/validation/index.d.ts +5 -2
  30. package/dist/jsxPlugins/validation/rules.d.ts +25 -0
  31. package/dist/transform/bi.d.ts +9 -0
  32. package/dist/transform/layoutError.d.ts +7 -4
  33. package/dist/transform/pipeline.d.ts +2 -2
  34. package/dist/transform/types.d.ts +11 -11
  35. package/package.json +3 -3
  36. package/src/cssPlugins/componentPosition.js +5 -1
  37. package/src/cssPlugins/customEnums.js +17 -11
  38. package/src/cssPlugins/customMedia.js +10 -3
  39. package/src/cssPlugins/fonts.js +5 -1
  40. package/src/cssPlugins/index.js +3 -1
  41. package/src/cssPlugins/layout/index.js +8 -1
  42. package/src/cssPlugins/layout/parser.js +15 -14
  43. package/src/cssPlugins/mixins.js +13 -6
  44. package/src/cssPlugins/presets.js +5 -1
  45. package/src/cssPlugins/shapeDividers.js +5 -1
  46. package/src/cssPlugins/spx/README.md +12 -3
  47. package/src/cssPlugins/spx/calls.js +28 -1
  48. package/src/cssPlugins/spx/cascade.js +26 -6
  49. package/src/cssPlugins/spx/index.js +8 -1
  50. package/src/cssPlugins/spxThemeVariables/README.md +14 -0
  51. package/src/cssPlugins/spxThemeVariables/index.js +36 -0
  52. package/src/cssPlugins/spxThemeVariables/utils/breakpointUtils.js +126 -0
  53. package/src/cssPlugins/spxThemeVariables/utils/copyPlanUtils.js +162 -0
  54. package/src/cssPlugins/spxThemeVariables/utils/cssTreeUtils.js +58 -0
  55. package/src/cssPlugins/spxThemeVariables/utils/restateUtils.js +134 -0
  56. package/src/cssPlugins/spxThemeVariables/utils/sectionUtils.js +32 -0
  57. package/src/cssPlugins/spxThemeVariables/utils/stylesheetUtils.js +98 -0
  58. package/src/cssPlugins/spxThemeVariables/utils/themeUtils.js +90 -0
  59. package/src/cssPlugins/spxThemeVariables/utils/wrappingAtRuleUtils.js +68 -0
  60. package/src/cssPlugins/spxThemeVariables/utils/writeUtils.js +98 -0
  61. package/src/cssPlugins/stackChildren.js +6 -2
  62. package/src/cssPlugins/wixVectorUrls.js +5 -1
  63. package/src/jsxPlugins/index.js +1 -1
  64. package/src/jsxPlugins/mediaValidation/index.js +10 -5
  65. package/src/jsxPlugins/origamiCompatibility/index.js +5 -1
  66. package/src/jsxPlugins/pageSlots.js +6 -1
  67. package/src/jsxPlugins/validation/checks.js +15 -8
  68. package/src/jsxPlugins/validation/index.js +9 -3
  69. package/src/jsxPlugins/validation/rules.js +14 -0
  70. package/src/transform/bi.js +30 -0
  71. package/src/transform/css/runPlugins.js +5 -7
  72. package/src/transform/jsx/runPlugins.js +3 -6
  73. package/src/transform/layoutError.js +7 -3
  74. package/src/transform/pipeline.js +2 -2
  75. package/src/transform/types.js +28 -6
@@ -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
+ }