@gesslar/sassy 0.19.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/src/Theme.js ADDED
@@ -0,0 +1,289 @@
1
+ /**
2
+ * @file Theme.js
3
+ *
4
+ * Defines the Theme class, representing a single theme compilation unit.
5
+ * Handles the complete lifecycle: loading source files, managing dependencies,
6
+ * compiling via Compiler, writing output, and supporting watch mode for live development.
7
+ * Maintains state for output, variable lookup, and resolution tracking.
8
+ *
9
+ * Responsibilities:
10
+ * - Load and validate theme source files
11
+ * - Track dependencies and variable resolution
12
+ * - Compile theme data into VS Code-compatible output
13
+ * - Write output files, supporting dry-run and hash-based skip
14
+ * - Support watch mode for live theme development
15
+ */
16
+ import Sass from "./Sass.js"
17
+ import Compiler from "./Compiler.js"
18
+ import DirectoryObject from "./DirectoryObject.js"
19
+ import File from "./File.js"
20
+ import FileObject from "./FileObject.js"
21
+ import Term from "./Term.js"
22
+ import ThemePool from "./ThemePool.js"
23
+ import Util from "./Util.js"
24
+
25
+ /**
26
+ * Theme class: manages the lifecycle of a theme compilation unit.
27
+ * See file-level docstring for responsibilities.
28
+ */
29
+ export default class Theme {
30
+ #sourceFile = null
31
+ #source = null
32
+ #options = null
33
+ #dependencies = []
34
+ #lookup = null
35
+ #pool = null
36
+ #cache = null
37
+ #name = null
38
+
39
+ // Write-related properties
40
+ #output = null
41
+ #outputJson = null
42
+ #outputFileName = null
43
+ #outputHash = null
44
+
45
+ #cwd = null
46
+
47
+ /**
48
+ * Creates a new Theme instance.
49
+ *
50
+ * @param {FileObject} themeFile - The source theme file object
51
+ * @param {DirectoryObject} cwd - The project's directory.
52
+ * @param {object} options - Compilation options
53
+ */
54
+ constructor(themeFile, cwd, options) {
55
+ this.#sourceFile = themeFile
56
+ this.#name = themeFile.module
57
+ this.#outputFileName = `${this.#name}.color-theme.json`
58
+ this.#options = options
59
+ this.#cwd = cwd
60
+ }
61
+
62
+ /**
63
+ * Resets the theme's compilation state, clearing output and lookup data.
64
+ * Used when recompiling in watch mode or clearing previous state.
65
+ */
66
+ reset() {
67
+ this.#output = null
68
+ this.#outputJson = null
69
+ this.#outputHash = null
70
+ this.#lookup = null
71
+ this.#pool = null
72
+ }
73
+
74
+ get cwd() {
75
+ return this.#cwd
76
+ }
77
+
78
+ get options() {
79
+ return this.#options
80
+ }
81
+
82
+ set cache(cache) {
83
+ if(!this.cache)
84
+ this.#cache=cache
85
+ }
86
+
87
+ get cache() {
88
+ return this.#cache
89
+ }
90
+
91
+ get name() {
92
+ return this.#name
93
+ }
94
+
95
+ /**
96
+ * Gets the source file object.
97
+ *
98
+ * @returns {FileObject} The source theme file
99
+ */
100
+ get sourceFile() {
101
+ return this.#sourceFile
102
+ }
103
+
104
+ /**
105
+ * Gets the compiled theme output object.
106
+ *
107
+ * @returns {object|null} The compiled theme output
108
+ */
109
+ get output() {
110
+ return this.#output
111
+ }
112
+
113
+ /**
114
+ * Sets the compiled theme output object and updates derived JSON and hash.
115
+ *
116
+ * @param {object} data - The compiled theme output object
117
+ */
118
+ set output(data) {
119
+ this.#output = data
120
+ this.#outputJson = JSON.stringify(data, null, 2) + "\n"
121
+ this.#outputHash = Util.hashOf(this.#outputJson)
122
+ }
123
+
124
+ /**
125
+ * Gets the array of file dependencies.
126
+ *
127
+ * @returns {FileObject[]} Array of dependency files
128
+ */
129
+ get dependencies() {
130
+ return this.#dependencies
131
+ }
132
+
133
+ /**
134
+ * Sets the array of file dependencies.
135
+ *
136
+ * @param {FileObject[]} data - Array of dependency files
137
+ */
138
+ set dependencies(data) {
139
+ this.#dependencies = data
140
+
141
+ if(!this.#dependencies.includes(this.#sourceFile))
142
+ this.#dependencies.unshift(this.#sourceFile)
143
+ }
144
+
145
+ /**
146
+ * Gets the parsed source data from the theme file.
147
+ *
148
+ * @returns {object|null} The parsed source data
149
+ */
150
+ get source() {
151
+ return this.#source
152
+ }
153
+
154
+ /**
155
+ * Gets the variable lookup data for theme compilation.
156
+ *
157
+ * @returns {object|null} The lookup data object
158
+ */
159
+ get lookup() {
160
+ return this.#lookup
161
+ }
162
+
163
+ /**
164
+ * Sets the variable lookup data for theme compilation.
165
+ *
166
+ * @param {object} data - The lookup data object
167
+ */
168
+ set lookup(data) {
169
+ this.#lookup = data
170
+ }
171
+
172
+ /**
173
+ * Gets the pool data for variable resolution tracking or null if one has
174
+ * not been set.
175
+ *
176
+ * @returns {ThemePool|null} The pool for this theme.
177
+ */
178
+ get pool() {
179
+ return this.#pool
180
+ }
181
+
182
+ /**
183
+ * Sets the pool data for variable resolution tracking. May not be over-
184
+ * written publicly. May only be reset
185
+ *
186
+ * @see reset
187
+ *
188
+ * @param {ThemePool} pool - The pool to assign to this theme
189
+ * @throws If there is already a pool.
190
+ */
191
+ set pool(pool) {
192
+ if(this.#pool)
193
+ throw Sass.new("Cannot override existing pool.")
194
+
195
+ this.#pool = pool
196
+ }
197
+
198
+ /**
199
+ * Method to return true or false if this theme has a pool.
200
+ *
201
+ * @returns {boolean} True if a pool has been set, false otherwise.
202
+ */
203
+ hasPool() {
204
+ return this.#pool instanceof ThemePool
205
+ }
206
+
207
+ /**
208
+ * Loads and parses the theme source file.
209
+ * Validates that the source contains required configuration.
210
+ *
211
+ * @returns {Promise<this>} Returns this instance for method chaining
212
+ * @throws {Sass} If source file lacks required 'config' property
213
+ */
214
+ async load() {
215
+ const source = await this.#cache.loadCachedData(this.#sourceFile)
216
+
217
+ if(!source.config)
218
+ throw Sass.new(
219
+ `Source file does not contain 'config' property: ${this.#sourceFile.path}`
220
+ )
221
+
222
+ this.#source = source
223
+ }
224
+
225
+ /**
226
+ * Adds a file dependency to the theme.
227
+ *
228
+ * @param {FileObject} file - The file to add as a dependency
229
+ * @returns {this} Returns this instance for method chaining
230
+ * @throws {Sass} If the file parameter is not a valid file
231
+ */
232
+ addDependency(file) {
233
+ if(!file.isFile)
234
+ throw Sass.new("File must be a dependency.")
235
+
236
+ this.#dependencies.push(file)
237
+
238
+ return this
239
+ }
240
+
241
+ /**
242
+ * Builds the theme by compiling source data into final output.
243
+ * Main entry point for theme compilation process.
244
+ *
245
+ * @returns {Promise<void>} Resolves when build is complete.
246
+ */
247
+ async build() {
248
+ const compiler = new Compiler()
249
+ await compiler.compile(this)
250
+ }
251
+
252
+ /**
253
+ * Writes the compiled theme output to a file or stdout.
254
+ * Handles dry-run mode, output directory creation, and duplicate write prevention.
255
+ *
256
+ * @param {boolean} [force] - Force a write. Used by the rebuild CLI option.
257
+ * @returns {Promise<void>} Resolves when write operation is complete
258
+ */
259
+ async write(force=false) {
260
+ const output = this.#outputJson
261
+ const outputDir = new DirectoryObject(this.#options.outputDir)
262
+ const file = new FileObject(this.#outputFileName, outputDir)
263
+
264
+ if(this.#options.dryRun) {
265
+ Term.log(this.#outputJson)
266
+
267
+ return {status: "dry-run", file}
268
+ }
269
+
270
+ // Skip identical bytes
271
+ if(!force) {
272
+ const nextHash = this.#outputHash
273
+ const lastHash = await file.exists
274
+ ? Util.hashOf(await File.readFile(file))
275
+ : "kakadoodoo"
276
+
277
+ if(lastHash === nextHash)
278
+ return {status: "skipped", file}
279
+ }
280
+
281
+ // Real write (timed)
282
+ if(!await outputDir.exists)
283
+ await File.assureDirectory(outputDir, {recursive: true})
284
+
285
+ await File.writeFile(file, output)
286
+
287
+ return {status: "written", bytes: output.length, file}
288
+ }
289
+ }
@@ -0,0 +1,139 @@
1
+
2
+ /**
3
+ * @file ThemePool.js
4
+ *
5
+ * Defines the ThemePool class, a collection of ThemeTokens for lookup and dependency tracking.
6
+ * Manages resolved values, raw resolutions, and token relationships during theme compilation.
7
+ */
8
+
9
+ import Sass from "./Sass.js"
10
+ import ThemeToken from "./ThemeToken.js"
11
+
12
+ /**
13
+ * ThemePool represents a collection of ThemeTokens serving both as a
14
+ * lookup of string>ThemeToken and dependencies.
15
+ *
16
+ * @class ThemePool
17
+ */
18
+ export default class ThemePool {
19
+ #tokens = new Map()
20
+ #resolved = new Map()
21
+ #rawResolved = new Map()
22
+
23
+ /**
24
+ * Returns the map of encoded theme token ids to their token object.
25
+ *
26
+ * @returns {Map<string, ThemeToken>} Map of tokens to their children.
27
+ */
28
+ get getTokens() {
29
+ return this.#tokens
30
+ }
31
+
32
+ /**
33
+ * Retrieves a resolved token by its name.
34
+ *
35
+ * @param {string} name - The token to look up.
36
+ * @returns {string|undefined} The resolved token string or undefined.
37
+ */
38
+ lookup(name) {
39
+ return this.#resolved.get(name)
40
+ }
41
+
42
+ /**
43
+ * Sets a resolved value for a token key.
44
+ *
45
+ * @param {string} key - The token key.
46
+ * @param {string} value - The resolved value.
47
+ */
48
+ resolve(key, value) {
49
+ this.#resolved.set(key, value)
50
+ }
51
+
52
+ /**
53
+ * Sets a raw resolved value for a token key.
54
+ *
55
+ * @param {string} key - The token key.
56
+ * @param {string} value - The raw resolved value.
57
+ */
58
+ rawResolve(key, value) {
59
+ this.#rawResolved.set(key, value)
60
+ }
61
+
62
+ /**
63
+ * Checks if a token name exists in resolved map.
64
+ *
65
+ * @param {string} name - The token name to check.
66
+ * @returns {boolean} True if the token exists.
67
+ */
68
+ has(name) {
69
+ return this.#resolved.has(name)
70
+ }
71
+
72
+ /**
73
+ * Checks if a token exists by its name.
74
+ *
75
+ * @param {ThemeToken} token - The token to check.
76
+ * @returns {boolean} True if the token exists.
77
+ */
78
+ hasToken(token) {
79
+ return this.has(token.name)
80
+ }
81
+
82
+ /**
83
+ * Retrieves a token's dependency.
84
+ *
85
+ * @param {ThemeToken} token - The token to look up.
86
+ * @returns {ThemeToken?} The dependent token with the given token, or undefined.
87
+ */
88
+ reverseLookup(token) {
89
+ return this.#tokens.get(token.getValue()) || null
90
+ }
91
+
92
+ /**
93
+ * Adds a token to the pool, optionally setting up dependencies if required.
94
+ *
95
+ * @param {ThemeToken} token - The token to add.
96
+ * @param {ThemeToken} [dependency] - The dependent token.
97
+ * @returns {ThemeToken} The token that was added.
98
+ */
99
+ addToken(token, dependency=null) {
100
+ if(!(token instanceof ThemeToken))
101
+ throw Sass.new("Token must be of type ThemeToken.")
102
+
103
+ if(!(dependency === null || dependency instanceof ThemeToken))
104
+ throw Sass.new("Token must be null or of type ThemeToken.")
105
+
106
+ this.#tokens.set(token.getName(), token)
107
+
108
+ return token
109
+ }
110
+
111
+ /**
112
+ * Finds a token by its value.
113
+ *
114
+ * @param {string} value - The value to search for.
115
+ * @returns {ThemeToken|undefined} The found token or undefined.
116
+ */
117
+ findToken(value) {
118
+ return [...this.#tokens.entries()].find(arg => arg[0] === value)?.[1]
119
+ }
120
+
121
+ /**
122
+ * Checks if one token is an ancestor of another using reverse lookup.
123
+ *
124
+ * @param {ThemeToken} candidate - Potential ancestor token.
125
+ * @param {ThemeToken} token - Potential descendant token.
126
+ * @returns {boolean} True if candidate is an ancestor of token.
127
+ */
128
+ isAncestorOf(candidate, token) {
129
+
130
+ do
131
+
132
+ if(candidate === token)
133
+ return true
134
+
135
+ while((token = this.reverseLookup(token)))
136
+
137
+ return false
138
+ }
139
+ }
@@ -0,0 +1,280 @@
1
+
2
+ /**
3
+ * @file ThemeToken.js
4
+ *
5
+ * Defines the ThemeToken class, representing a single token in a theme tree.
6
+ * Encapsulates token data, relationships, and provides methods for property
7
+ * management, pool integration, and serialization during theme compilation.
8
+ */
9
+
10
+ import Sass from "./Sass.js"
11
+ import ThemePool from "./ThemePool.js"
12
+
13
+ /**
14
+ * ThemeToken represents a single token in a theme tree, encapsulating theme
15
+ * token data and relationships.
16
+ *
17
+ * Provides property management, factory methods, tree integration, and
18
+ * serialization.
19
+ *
20
+ * @class ThemeToken
21
+ */
22
+ export default class ThemeToken {
23
+ #pool
24
+
25
+ #name = null
26
+ #kind = null
27
+ #rawValue = null
28
+ #value = null
29
+ #dependency = null
30
+ #parentTokenKey = null
31
+ #trail = new Array()
32
+ #parsedColor = null
33
+
34
+ /**
35
+ * Constructs a ThemeToken with a given token name.
36
+ *
37
+ * @param {string} name - The token name for this token.
38
+ */
39
+ constructor(name) {
40
+ if(typeof name !== "string")
41
+ throw Sass.new("Token name must be a bare string.")
42
+
43
+ this.setName(name)
44
+ }
45
+
46
+ /**
47
+ * Adds this token to a ThemePool with optional dependency.
48
+ *
49
+ * @param {ThemePool} pool - The pool to add to.
50
+ * @param {ThemeToken} [dependency] - Optional dependency token.
51
+ * @returns {ThemeToken} This token instance.
52
+ */
53
+ addToPool(pool=null, dependency) {
54
+ if(!(pool instanceof ThemePool))
55
+ throw Sass.new("Pool must be a ThemePool instance.")
56
+
57
+ if(this.#pool)
58
+ return this
59
+
60
+ if(!(dependency == null || dependency instanceof ThemeToken))
61
+ throw Sass.new("Dependency must be null or of type ThemeToken.")
62
+
63
+ this.#pool = pool
64
+
65
+ return pool.addToken(this, dependency)
66
+ }
67
+
68
+ /**
69
+ * Sets the name of this token (only if not already set).
70
+ *
71
+ * @param {string} name - The token name.
72
+ * @returns {ThemeToken} This token instance.
73
+ */
74
+ setName(name) {
75
+ if(!this.#name)
76
+ this.#name = name
77
+
78
+ return this
79
+ }
80
+
81
+ /**
82
+ * Gets the name of this token.
83
+ *
84
+ * @returns {string} The token name.
85
+ */
86
+ getName() {
87
+ return this.#name
88
+ }
89
+
90
+ /**
91
+ * Sets the kind of this token (only if not already set).
92
+ *
93
+ * @param {string} kind - The token kind.
94
+ * @returns {ThemeToken} This token instance.
95
+ */
96
+ setKind(kind) {
97
+ if(!this.#kind)
98
+ this.#kind = kind
99
+
100
+ return this
101
+ }
102
+
103
+ /**
104
+ * Gets the kind of this token.
105
+ *
106
+ * @returns {string} The token kind.
107
+ */
108
+ getKind() {
109
+ return this.#kind
110
+ }
111
+
112
+ /**
113
+ * Sets the value of this token.
114
+ *
115
+ * @param {string} value - The token value.
116
+ * @returns {ThemeToken} This token instance.
117
+ */
118
+ setValue(value) {
119
+ this.#value = value
120
+
121
+ return this
122
+ }
123
+
124
+ /**
125
+ * Gets the value of this token.
126
+ *
127
+ * @returns {string} The token value.
128
+ */
129
+ getValue() {
130
+ return this.#value
131
+ }
132
+
133
+ /**
134
+ * Sets the raw value of this token (only if not already set).
135
+ *
136
+ * @param {string} raw - The raw token value.
137
+ * @returns {ThemeToken} This token instance.
138
+ */
139
+ setRawValue(raw) {
140
+ if(!this.#rawValue)
141
+ this.#rawValue = raw
142
+
143
+ return this
144
+ }
145
+
146
+ /**
147
+ * Gets the raw value of this token.
148
+ *
149
+ * @returns {string} The raw token value.
150
+ */
151
+ getRawValue() {
152
+ return this.#rawValue
153
+ }
154
+
155
+ /**
156
+ * Sets the dependency of this token (only if not already set).
157
+ *
158
+ * @param {ThemeToken} dependency - The dependency token.
159
+ * @returns {ThemeToken} This token instance.
160
+ */
161
+ setDependency(dependency) {
162
+ if(!this.#dependency)
163
+ this.#dependency = dependency
164
+
165
+ return this
166
+ }
167
+
168
+ /**
169
+ * Gets the dependency of this token.
170
+ *
171
+ * @returns {ThemeToken|null} The dependency token or null.
172
+ */
173
+ getDependency() {
174
+ return this.#dependency || null
175
+ }
176
+
177
+ /**
178
+ * Sets the parent token key.
179
+ *
180
+ * @param {string} tokenKey - The parent token key.
181
+ * @returns {ThemeToken} This token instance.
182
+ */
183
+ setParentTokenKey(tokenKey) {
184
+ this.#parentTokenKey = tokenKey
185
+
186
+ return this
187
+ }
188
+
189
+ /**
190
+ * Gets the parent token key.
191
+ *
192
+ * @returns {string|null} The parent token key or null.
193
+ */
194
+ getParentTokenKey() {
195
+ return this.#parentTokenKey || null
196
+ }
197
+
198
+ /**
199
+ * Adds a trail of tokens to this token's trail array.
200
+ *
201
+ * @param {Array<ThemeToken>} trail - Array of tokens to add.
202
+ * @returns {ThemeToken} This token instance.
203
+ */
204
+ addTrail(trail) {
205
+ const current = this.#trail
206
+
207
+ trail.forEach(value => {
208
+ if(!current.includes(value))
209
+ current.push(value)
210
+ })
211
+
212
+ return this
213
+ }
214
+
215
+ /**
216
+ * Gets the trail array of this token.
217
+ *
218
+ * @returns {Array<ThemeToken>} The trail array.
219
+ */
220
+ getTrail() {
221
+ return this.#trail
222
+ }
223
+
224
+ /**
225
+ * Sets the parsed color object for this token.
226
+ *
227
+ * @param {object} parsedColor - The parsed Culori color object
228
+ * @returns {ThemeToken} This token instance.
229
+ */
230
+ setParsedColor(parsedColor) {
231
+ this.#parsedColor = parsedColor
232
+ return this
233
+ }
234
+
235
+ /**
236
+ * Gets the parsed color object of this token.
237
+ *
238
+ * @returns {object|null} The parsed Culori color object or null.
239
+ */
240
+ getParsedColor() {
241
+ return this.#parsedColor
242
+ }
243
+
244
+ /**
245
+ * Checks if this token has an ancestor with the given token name.
246
+ *
247
+ * @param {string} name - The name of the ancestor token to check for.
248
+ * @returns {boolean} True if ancestor exists.
249
+ */
250
+ hasDependency(name) {
251
+ return this.#dependency && this.#dependency.getName() === name
252
+ }
253
+
254
+ /**
255
+ * Gets the ThemePool associated with this token.
256
+ *
257
+ * @returns {ThemePool} The associated pool.
258
+ */
259
+ getPool() {
260
+ return this.#pool
261
+ }
262
+
263
+ /**
264
+ * Returns a JSON representation of the ThemeToken.
265
+ *
266
+ * @returns {object} JSON representation of the ThemeToken
267
+ */
268
+ toJSON() {
269
+ return {
270
+ name: this.#name,
271
+ kind: this.#kind,
272
+ rawValue: this.#rawValue,
273
+ value: this.#value,
274
+ dependency: this.#dependency?.toJSON() ?? null,
275
+ parentTokenKey: this.#parentTokenKey,
276
+ trail: this.#trail,
277
+ parsedColor: this.#parsedColor
278
+ }
279
+ }
280
+ }