@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/README.md +605 -0
- package/UNLICENSE.txt +24 -0
- package/package.json +60 -0
- package/src/BuildCommand.js +183 -0
- package/src/Cache.js +73 -0
- package/src/Colour.js +414 -0
- package/src/Command.js +212 -0
- package/src/Compiler.js +310 -0
- package/src/Data.js +545 -0
- package/src/DirectoryObject.js +188 -0
- package/src/Evaluator.js +348 -0
- package/src/File.js +334 -0
- package/src/FileObject.js +226 -0
- package/src/LintCommand.js +498 -0
- package/src/ResolveCommand.js +433 -0
- package/src/Sass.js +165 -0
- package/src/Session.js +360 -0
- package/src/Term.js +175 -0
- package/src/Theme.js +289 -0
- package/src/ThemePool.js +139 -0
- package/src/ThemeToken.js +280 -0
- package/src/Type.js +206 -0
- package/src/Util.js +132 -0
- package/src/Valid.js +50 -0
- package/src/cli.js +155 -0
package/src/Command.js
ADDED
|
@@ -0,0 +1,212 @@
|
|
|
1
|
+
import Sass from "./Sass.js"
|
|
2
|
+
import FileObject from "./FileObject.js"
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* Base class for command-line interface commands.
|
|
6
|
+
* Provides common functionality for CLI option handling and file resolution.
|
|
7
|
+
*/
|
|
8
|
+
export default class Command {
|
|
9
|
+
#cliCommand = null
|
|
10
|
+
#cliOptions = null
|
|
11
|
+
#optionNames = []
|
|
12
|
+
#command
|
|
13
|
+
#cwd
|
|
14
|
+
#packageJson
|
|
15
|
+
|
|
16
|
+
#cache
|
|
17
|
+
|
|
18
|
+
/**
|
|
19
|
+
* Creates a new Command instance.
|
|
20
|
+
*
|
|
21
|
+
* @param {object} config - Configuration object
|
|
22
|
+
* @param {object} config.cwd - Current working directory object
|
|
23
|
+
* @param {object} config.packageJson - Package.json data
|
|
24
|
+
*/
|
|
25
|
+
constructor({cwd,packageJson}) {
|
|
26
|
+
this.#cwd = cwd
|
|
27
|
+
this.#packageJson = packageJson
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
get cache() {
|
|
31
|
+
return this.#cache
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
set cache(cache) {
|
|
35
|
+
if(!this.#cache)
|
|
36
|
+
this.#cache = cache
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
/**
|
|
40
|
+
* Gets the current working directory object.
|
|
41
|
+
*
|
|
42
|
+
* @returns {object} The current working directory
|
|
43
|
+
*/
|
|
44
|
+
get cwd() {
|
|
45
|
+
return this.#cwd
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
/**
|
|
49
|
+
* Gets the package.json data.
|
|
50
|
+
*
|
|
51
|
+
* @returns {object} The package.json object
|
|
52
|
+
*/
|
|
53
|
+
get packageJson() {
|
|
54
|
+
return this.#packageJson
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
/**
|
|
58
|
+
* Gets the CLI command string.
|
|
59
|
+
*
|
|
60
|
+
* @returns {string|null} The CLI command string
|
|
61
|
+
*/
|
|
62
|
+
get cliCommand() {
|
|
63
|
+
return this.#cliCommand
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
/**
|
|
67
|
+
* Sets the CLI command string.
|
|
68
|
+
*
|
|
69
|
+
* @param {string} data - The CLI command string
|
|
70
|
+
*/
|
|
71
|
+
set cliCommand(data) {
|
|
72
|
+
this.#cliCommand = data
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
/**
|
|
76
|
+
* Gets the CLI options object.
|
|
77
|
+
*
|
|
78
|
+
* @returns {object|null} The CLI options configuration
|
|
79
|
+
*/
|
|
80
|
+
get cliOptions() {
|
|
81
|
+
return this.#cliOptions
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
/**
|
|
85
|
+
* Sets the CLI options object.
|
|
86
|
+
*
|
|
87
|
+
* @param {object} data - The CLI options configuration
|
|
88
|
+
*/
|
|
89
|
+
set cliOptions(data) {
|
|
90
|
+
this.#cliOptions = data
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
/**
|
|
94
|
+
* Gets the array of CLI option names.
|
|
95
|
+
*
|
|
96
|
+
* @returns {string[]} Array of option names
|
|
97
|
+
*/
|
|
98
|
+
get cliOptionNames() {
|
|
99
|
+
return this.#optionNames
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
/**
|
|
103
|
+
* Builds the CLI command interface using the commander.js program instance.
|
|
104
|
+
* Initializes the command with its options and action handler.
|
|
105
|
+
*
|
|
106
|
+
* @param {object} program - The commander.js program instance
|
|
107
|
+
* @returns {Promise<this>} Returns this instance for method chaining
|
|
108
|
+
*/
|
|
109
|
+
async buildCli(program) {
|
|
110
|
+
if(!this.cliCommand)
|
|
111
|
+
throw Sass.new("This command has no CLI command string.")
|
|
112
|
+
|
|
113
|
+
if(!this.cliOptions)
|
|
114
|
+
throw Sass.new("This command has no CLI options.")
|
|
115
|
+
|
|
116
|
+
this.#command = program.command(this.cliCommand)
|
|
117
|
+
this.#command.action(async(...arg) => {
|
|
118
|
+
try {
|
|
119
|
+
await this.execute(...arg)
|
|
120
|
+
} catch(error) {
|
|
121
|
+
throw Sass.new(`Trying to execute ${this.constructor.name} with ${JSON.stringify(...arg)}`, error)
|
|
122
|
+
}
|
|
123
|
+
})
|
|
124
|
+
|
|
125
|
+
this.addCliOptions(this.cliOptions, true)
|
|
126
|
+
|
|
127
|
+
return this
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
/**
|
|
131
|
+
* Adds a single CLI option to the command.
|
|
132
|
+
*
|
|
133
|
+
* @param {string} name - The option name
|
|
134
|
+
* @param {string[]} options - Array containing option flag and description
|
|
135
|
+
* @param {boolean} preserve - Whether to preserve this option name in the list
|
|
136
|
+
* @returns {this} Returns this instance for method chaining
|
|
137
|
+
*/
|
|
138
|
+
addCliOption(name, options, preserve) {
|
|
139
|
+
if(!this.#command)
|
|
140
|
+
throw new Error("Unitialised Command")
|
|
141
|
+
|
|
142
|
+
this.#command.option(...options)
|
|
143
|
+
|
|
144
|
+
if(preserve === true)
|
|
145
|
+
this.#optionNames.push(name)
|
|
146
|
+
|
|
147
|
+
return this
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
/**
|
|
151
|
+
* Adds multiple CLI options to the command.
|
|
152
|
+
*
|
|
153
|
+
* @param {object} options - Object mapping option names to [flag, description] arrays
|
|
154
|
+
* @param {boolean} preserve - Whether to preserve option names in the list
|
|
155
|
+
* @returns {this} Returns this instance for method chaining
|
|
156
|
+
*/
|
|
157
|
+
addCliOptions(options, preserve) {
|
|
158
|
+
for(const [name, opts] of Object.entries(options))
|
|
159
|
+
this.addCliOption(name, opts, preserve)
|
|
160
|
+
|
|
161
|
+
return this
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
/**
|
|
165
|
+
* Resolves a theme file name to a FileObject and validates its existence.
|
|
166
|
+
*
|
|
167
|
+
* @param {string} fileName - The theme file name or path
|
|
168
|
+
* @param {object} cwd - The current working directory object
|
|
169
|
+
* @returns {Promise<FileObject>} The resolved and validated FileObject
|
|
170
|
+
* @throws {Sass} If the file does not exist
|
|
171
|
+
*/
|
|
172
|
+
async resolveThemeFileName(fileName, cwd) {
|
|
173
|
+
const fileObject = new FileObject(fileName, cwd)
|
|
174
|
+
|
|
175
|
+
if(!await fileObject.exists)
|
|
176
|
+
throw Sass.new(`No such file 🤷: ${fileObject.path}`)
|
|
177
|
+
|
|
178
|
+
return fileObject
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
/**
|
|
182
|
+
* Emits an event asynchronously and waits for all listeners to complete.
|
|
183
|
+
* Unlike the standard EventEmitter.emit() which is synchronous, this method
|
|
184
|
+
* properly handles async event listeners by waiting for all of them to
|
|
185
|
+
* resolve or reject using Promise.allSettled().
|
|
186
|
+
*
|
|
187
|
+
* @param {string} event - The event name to emit
|
|
188
|
+
* @param {any[]} [arg] - Arguments to pass to event listeners
|
|
189
|
+
* @returns {Promise<void>} Resolves when all listeners have completed
|
|
190
|
+
*/
|
|
191
|
+
async asyncEmit(event, arg) {
|
|
192
|
+
try {
|
|
193
|
+
arg = arg || new Array()
|
|
194
|
+
const listeners = this.emitter.listeners(event)
|
|
195
|
+
|
|
196
|
+
const settled = await Promise.allSettled(listeners.map(listener => listener(arg)))
|
|
197
|
+
const rejected = settled.filter(reject => reject.status === "rejected")
|
|
198
|
+
|
|
199
|
+
if(rejected.length > 0) {
|
|
200
|
+
if(rejected[0].reason instanceof Error)
|
|
201
|
+
throw rejected[0].reason
|
|
202
|
+
else
|
|
203
|
+
throw Sass.new(rejected[0].reason)
|
|
204
|
+
}
|
|
205
|
+
} catch(error) {
|
|
206
|
+
throw Sass.new(
|
|
207
|
+
`Processing '${event}' event with ${arg&&arg.length?`'${arg}'`:"no arguments"}.`,
|
|
208
|
+
error
|
|
209
|
+
)
|
|
210
|
+
}
|
|
211
|
+
}
|
|
212
|
+
}
|
package/src/Compiler.js
ADDED
|
@@ -0,0 +1,310 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @file Compiler.js
|
|
3
|
+
*
|
|
4
|
+
* Defines the Compiler class, the main engine for processing theme configuration files.
|
|
5
|
+
* Handles all phases of theme compilation:
|
|
6
|
+
* 1. Import resolution (merging modular theme files)
|
|
7
|
+
* 2. Variable decomposition and flattening
|
|
8
|
+
* 3. Token evaluation and colour function application
|
|
9
|
+
* 4. Recursive resolution of references
|
|
10
|
+
* 5. Output assembly for VS Code themes
|
|
11
|
+
* Supports extension points for custom phases and output formats.
|
|
12
|
+
*/
|
|
13
|
+
|
|
14
|
+
import Sass from "./Sass.js"
|
|
15
|
+
import Data from "./Data.js"
|
|
16
|
+
import Evaluator from "./Evaluator.js"
|
|
17
|
+
import File from "./File.js"
|
|
18
|
+
import FileObject from "./FileObject.js"
|
|
19
|
+
import Term from "./Term.js"
|
|
20
|
+
import Theme from "./Theme.js"
|
|
21
|
+
import Util from "./Util.js"
|
|
22
|
+
|
|
23
|
+
/**
|
|
24
|
+
* Main compiler class for processing theme source files.
|
|
25
|
+
* Handles the complete compilation pipeline from source to VS Code theme output.
|
|
26
|
+
*/
|
|
27
|
+
export default class Compiler {
|
|
28
|
+
/**
|
|
29
|
+
* Compiles a theme source file into a VS Code colour theme.
|
|
30
|
+
* Processes configuration, variables, imports, and theme definitions.
|
|
31
|
+
*
|
|
32
|
+
* @param {object} theme - The file object containing source data and metadata
|
|
33
|
+
* @returns {Promise<void>} Resolves when compilation is complete
|
|
34
|
+
*/
|
|
35
|
+
async compile(theme) {
|
|
36
|
+
try {
|
|
37
|
+
const source = theme.source
|
|
38
|
+
const {config: sourceConfig} = source ?? {}
|
|
39
|
+
const {vars: sourceVars} = source
|
|
40
|
+
const {theme: sourceTheme} = source
|
|
41
|
+
|
|
42
|
+
const evaluator = new Evaluator()
|
|
43
|
+
const evaluate = (...arg) => evaluator.evaluate(...arg)
|
|
44
|
+
|
|
45
|
+
const config = this.#decomposeObject(sourceConfig)
|
|
46
|
+
evaluate(config)
|
|
47
|
+
const recompConfig = this.#composeObject(config)
|
|
48
|
+
|
|
49
|
+
const header = {
|
|
50
|
+
$schema: recompConfig.$schema,
|
|
51
|
+
name: recompConfig.name,
|
|
52
|
+
type: recompConfig.type
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
// Let's get all of the imports!
|
|
56
|
+
const imports = recompConfig.import ?? []
|
|
57
|
+
const {imported,importedFiles} = await this.#import(imports, theme)
|
|
58
|
+
|
|
59
|
+
theme.dependencies = importedFiles
|
|
60
|
+
|
|
61
|
+
const merged = Data.mergeObject({},
|
|
62
|
+
imported,
|
|
63
|
+
{
|
|
64
|
+
vars: sourceVars ?? {},
|
|
65
|
+
colors: sourceTheme?.colors ?? {},
|
|
66
|
+
tokenColors: sourceTheme?.tokenColors ?? [],
|
|
67
|
+
semanticTokenColors: sourceTheme?.semanticTokenColors ?? {},
|
|
68
|
+
}
|
|
69
|
+
)
|
|
70
|
+
|
|
71
|
+
// Shred them up! Kinda. And evaluate the variables in place
|
|
72
|
+
const vars = this.#decomposeObject(merged.vars)
|
|
73
|
+
evaluate(vars)
|
|
74
|
+
const workColors = this.#decomposeObject(merged.colors)
|
|
75
|
+
evaluate(workColors)
|
|
76
|
+
const workTokenColors = this.#decomposeObject(merged.tokenColors)
|
|
77
|
+
evaluate(workTokenColors)
|
|
78
|
+
const workSemanticTokenColors = this.#decomposeObject(merged.semanticTokenColors)
|
|
79
|
+
evaluate(workSemanticTokenColors)
|
|
80
|
+
|
|
81
|
+
theme.lookup = evaluator.lookup
|
|
82
|
+
|
|
83
|
+
// Now let's do some reducing... into a form that works for VS Code
|
|
84
|
+
const reducer = (acc,curr) => {
|
|
85
|
+
acc[curr.flatPath] = curr.value
|
|
86
|
+
return acc
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
// Assemble into one object with the proper keys
|
|
90
|
+
const colors = workColors.reduce(reducer, {})
|
|
91
|
+
const tokenColors = this.#composeArray(workTokenColors)
|
|
92
|
+
const semanticTokenColors = workSemanticTokenColors.reduce(reducer, {})
|
|
93
|
+
|
|
94
|
+
// Mix and maaatch all jumbly wumbly...
|
|
95
|
+
const output = Data.mergeObject(
|
|
96
|
+
{},
|
|
97
|
+
header,
|
|
98
|
+
sourceConfig.custom ?? {},
|
|
99
|
+
{
|
|
100
|
+
colors,
|
|
101
|
+
semanticTokenColors,
|
|
102
|
+
tokenColors
|
|
103
|
+
}
|
|
104
|
+
)
|
|
105
|
+
|
|
106
|
+
// Voilà!
|
|
107
|
+
theme.output = output
|
|
108
|
+
theme.pool = evaluator.pool
|
|
109
|
+
} catch(error) {
|
|
110
|
+
throw Sass.new(`Compiling ${theme.name}`, error)
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
/**
|
|
115
|
+
* Imports external theme files and merges their content.
|
|
116
|
+
* Processes import specifications and loads referenced files.
|
|
117
|
+
*
|
|
118
|
+
* @param {Array<string>} imports - The import filenames.
|
|
119
|
+
* @param {Theme} theme - The theme object being compiled.
|
|
120
|
+
* @returns {Promise<object>} Object containing imported data and file references
|
|
121
|
+
*/
|
|
122
|
+
async #import(imports, theme) {
|
|
123
|
+
const importedFiles = []
|
|
124
|
+
const imported = {
|
|
125
|
+
vars: {},
|
|
126
|
+
colors: {},
|
|
127
|
+
tokenColors: []
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
imports = typeof imports === "string"
|
|
131
|
+
? [imports]
|
|
132
|
+
: imports
|
|
133
|
+
|
|
134
|
+
|
|
135
|
+
if(!Data.isArrayUniform(imports, "string"))
|
|
136
|
+
throw new Sass(
|
|
137
|
+
`All import entries must be strings. Got ${JSON.stringify(imports)}`
|
|
138
|
+
)
|
|
139
|
+
|
|
140
|
+
const loaded = []
|
|
141
|
+
|
|
142
|
+
for(const importing of imports) {
|
|
143
|
+
try {
|
|
144
|
+
const file = new FileObject(importing, theme.sourceFile.directory)
|
|
145
|
+
|
|
146
|
+
importedFiles.push(file)
|
|
147
|
+
|
|
148
|
+
// Get the cached version or a new version. Who knows? I don't know.
|
|
149
|
+
const {result, cost} = await Util.time(async() => {
|
|
150
|
+
return await theme.cache.loadCachedData(file)
|
|
151
|
+
})
|
|
152
|
+
|
|
153
|
+
if(theme.options.nerd) {
|
|
154
|
+
Term.status([
|
|
155
|
+
["muted", Util.rightAlignText(`${cost.toLocaleString()}ms`, 10), ["[","]"]],
|
|
156
|
+
"",
|
|
157
|
+
["muted", `${File.relativeOrAbsolutePath(theme.cwd,file)}`],
|
|
158
|
+
["muted", `${theme.name}`,["(",")"]],
|
|
159
|
+
], theme.options)
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
if(result) {
|
|
163
|
+
loaded.push(result)
|
|
164
|
+
}
|
|
165
|
+
} catch(error) {
|
|
166
|
+
throw Sass.new(`Attempting to import ${importing}`, error)
|
|
167
|
+
}
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
loaded.forEach(data => {
|
|
171
|
+
const {vars={}} = data ?? {}
|
|
172
|
+
const {colors={},tokenColors=[]} = data.theme ?? {}
|
|
173
|
+
|
|
174
|
+
imported.vars = Data.mergeObject(imported.vars, vars)
|
|
175
|
+
imported.colors = Data.mergeObject(imported.colors, colors)
|
|
176
|
+
imported.tokenColors = Data.mergeArray(imported.tokenColors, tokenColors)
|
|
177
|
+
})
|
|
178
|
+
|
|
179
|
+
return {imported,importedFiles}
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
/**
|
|
183
|
+
* Decomposes a nested object into flat entries with path information.
|
|
184
|
+
* Recursively processes objects and arrays to create a flat structure for
|
|
185
|
+
* evaluation.
|
|
186
|
+
*
|
|
187
|
+
* @param {object} work - The object to decompose
|
|
188
|
+
* @param {string[]} path - Current path array for nested properties
|
|
189
|
+
* @returns {Array<object>} Array of decomposed object entries with path information
|
|
190
|
+
*/
|
|
191
|
+
#decomposeObject(work, path = []) {
|
|
192
|
+
const isObject = this.#isObject
|
|
193
|
+
|
|
194
|
+
const result = []
|
|
195
|
+
|
|
196
|
+
for(const key in work) {
|
|
197
|
+
const currPath = [...path, key]
|
|
198
|
+
const item = work[key]
|
|
199
|
+
|
|
200
|
+
if(isObject(item)) {
|
|
201
|
+
result.push(...this.#decomposeObject(work[key], currPath))
|
|
202
|
+
} else if(Array.isArray(work[key])) {
|
|
203
|
+
item.forEach((item, index) => {
|
|
204
|
+
const path = [...currPath, String(index+1)]
|
|
205
|
+
result.push({
|
|
206
|
+
key,
|
|
207
|
+
value: String(item),
|
|
208
|
+
path,
|
|
209
|
+
flatPath: path.join("."),
|
|
210
|
+
array: {
|
|
211
|
+
path: path.slice(0, -1),
|
|
212
|
+
flatPath: path.slice(0, -1).join("."),
|
|
213
|
+
index
|
|
214
|
+
}
|
|
215
|
+
})
|
|
216
|
+
})
|
|
217
|
+
} else {
|
|
218
|
+
result.push({key, value: String(item), path, flatPath: currPath.join(".")})
|
|
219
|
+
}
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
return result
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
/**
|
|
226
|
+
* Recomposes a decomposed object array back into a hierarchical object structure.
|
|
227
|
+
* Reconstructs nested objects from the flat representation created by decomposeObject.
|
|
228
|
+
*
|
|
229
|
+
* @param {Array<object>} decomposed - Array of decomposed object entries
|
|
230
|
+
* @returns {object} The recomposed hierarchical object
|
|
231
|
+
*/
|
|
232
|
+
#composeObject(decomposed) {
|
|
233
|
+
const done = []
|
|
234
|
+
|
|
235
|
+
return decomposed.reduce((acc, curr, index, arr) => {
|
|
236
|
+
// Test for an array
|
|
237
|
+
if("array" in curr) {
|
|
238
|
+
const array = curr.array
|
|
239
|
+
const fp = array.flatPath
|
|
240
|
+
|
|
241
|
+
if(done.includes(array.flatPath))
|
|
242
|
+
return acc
|
|
243
|
+
|
|
244
|
+
const matches = arr.filter(a => "array" in a && a.array.flatPath === fp)
|
|
245
|
+
const fps = matches.map(m => m.array.flatPath)
|
|
246
|
+
const sorted = matches.sort((a,b) => a.array.index - b.array.index)
|
|
247
|
+
const value = sorted.map(m => m.value)
|
|
248
|
+
|
|
249
|
+
done.push(...fps)
|
|
250
|
+
Data.setNestedValue(acc, array.path, value)
|
|
251
|
+
} else {
|
|
252
|
+
if(done.includes(curr.flatPath))
|
|
253
|
+
return acc
|
|
254
|
+
|
|
255
|
+
const keyPath = [...curr.path, curr.key]
|
|
256
|
+
|
|
257
|
+
done.push(curr.flatPath)
|
|
258
|
+
Data.setNestedValue(acc, keyPath, curr.value)
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
return acc
|
|
262
|
+
}, {})
|
|
263
|
+
}
|
|
264
|
+
|
|
265
|
+
/**
|
|
266
|
+
* Composes decomposed object entries into array structures.
|
|
267
|
+
* Reconstructs array-based configurations from decomposed format.
|
|
268
|
+
*
|
|
269
|
+
* @param {Array<object>} decomposed - Array of decomposed object entries
|
|
270
|
+
* @returns {Array} The composed array structure
|
|
271
|
+
*/
|
|
272
|
+
#composeArray(decomposed) {
|
|
273
|
+
const sections = decomposed.reduce((acc,curr) => {
|
|
274
|
+
if(!acc.includes(curr.path[0]))
|
|
275
|
+
acc.push(curr.path[0])
|
|
276
|
+
|
|
277
|
+
return acc
|
|
278
|
+
}, [])
|
|
279
|
+
const sorted = sections.sort((a,b) => parseInt(a) - parseInt(b))
|
|
280
|
+
|
|
281
|
+
return sorted.map(curr => {
|
|
282
|
+
const section = decomposed
|
|
283
|
+
.filter(c => c.path[0] === curr)
|
|
284
|
+
.map(c => {
|
|
285
|
+
const [_, newFlatPath] = c.flatPath.match(/^\w+\.(.*)$/)
|
|
286
|
+
const newPath = c.path.slice(1)
|
|
287
|
+
|
|
288
|
+
return Object.assign(c, {
|
|
289
|
+
path: newPath,
|
|
290
|
+
flatPath: newFlatPath
|
|
291
|
+
})
|
|
292
|
+
})
|
|
293
|
+
|
|
294
|
+
return this.#composeObject(section)
|
|
295
|
+
})
|
|
296
|
+
}
|
|
297
|
+
|
|
298
|
+
/**
|
|
299
|
+
* Checks if a value is a plain object (not null or array).
|
|
300
|
+
* Utility method for type checking during compilation.
|
|
301
|
+
*
|
|
302
|
+
* @param {*} value - The value to check
|
|
303
|
+
* @returns {boolean} True if the value is a plain object
|
|
304
|
+
*/
|
|
305
|
+
#isObject(value) {
|
|
306
|
+
return typeof value === "object" &&
|
|
307
|
+
value !== null &&
|
|
308
|
+
!Array.isArray(value)
|
|
309
|
+
}
|
|
310
|
+
}
|